package libcontainer

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
	"github.com/moby/sys/mountinfo"
	"github.com/moby/sys/userns"
	"github.com/mrunalp/fileutils"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/opencontainers/selinux/go-selinux/label"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"

	"github.com/opencontainers/cgroups"
	devices "github.com/opencontainers/cgroups/devices/config"
	"github.com/opencontainers/cgroups/fs2"
	"github.com/opencontainers/runc/internal/pathrs"
	"github.com/opencontainers/runc/internal/sys"
	"github.com/opencontainers/runc/libcontainer/configs"
	"github.com/opencontainers/runc/libcontainer/utils"
)

const defaultMountFlags = unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV

// mountConfig contains mount data not specific to a mount point.
type mountConfig struct {
	root            string
	label           string
	cgroup2Path     string
	rootlessCgroups bool
	cgroupns        bool
}

// mountEntry contains mount data specific to a mount point.
type mountEntry struct {
	*configs.Mount
	srcFile *mountSource
	dstFile *os.File
}

// srcName is only meant for error messages, it returns a "friendly" name.
func (m mountEntry) srcName() string {
	if m.srcFile != nil {
		return m.srcFile.file.Name()
	}
	return m.Source
}

func (m mountEntry) srcStat() (os.FileInfo, *syscall.Stat_t, error) {
	var (
		st  os.FileInfo
		err error
	)
	if m.srcFile != nil {
		st, err = m.srcFile.file.Stat()
	} else {
		st, err = os.Stat(m.Source)
	}
	if err != nil {
		return nil, nil, err
	}
	return st, st.Sys().(*syscall.Stat_t), nil
}

func (m mountEntry) srcStatfs() (*unix.Statfs_t, error) {
	var st unix.Statfs_t
	if m.srcFile != nil {
		if err := unix.Fstatfs(int(m.srcFile.file.Fd()), &st); err != nil {
			return nil, os.NewSyscallError("fstatfs", err)
		}
	} else {
		if err := unix.Statfs(m.Source, &st); err != nil {
			return nil, &os.PathError{Op: "statfs", Path: m.Source, Err: err}
		}
	}
	return &st, nil
}

// needsSetupDev returns true if /dev needs to be set up.
func needsSetupDev(config *configs.Config) bool {
	for _, m := range config.Mounts {
		if m.Device == "bind" && utils.CleanPath(m.Destination) == "/dev" {
			return false
		}
	}
	return true
}

// prepareRootfs sets up the devices, mount points, and filesystems for use
// inside a new mount namespace. It doesn't set anything as ro. You must call
// finalizeRootfs after this function to finish setting up the rootfs.
func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) {
	config := iConfig.Config
	if err := prepareRoot(config); err != nil {
		return fmt.Errorf("error preparing rootfs: %w", err)
	}

	mountConfig := &mountConfig{
		root:            config.Rootfs,
		label:           config.MountLabel,
		cgroup2Path:     iConfig.Cgroup2Path,
		rootlessCgroups: config.RootlessCgroups,
		cgroupns:        config.Namespaces.Contains(configs.NEWCGROUP),
	}
	for _, m := range config.Mounts {
		entry := mountEntry{Mount: m}
		// Figure out whether we need to request runc to give us an
		// open_tree(2)-style mountfd. For idmapped mounts, this is always
		// necessary. For bind-mounts, this is only necessary if we cannot
		// resolve the parent mount (this is only hit if you are running in a
		// userns -- but for rootless the host-side thread can't help).
		wantSourceFile := m.IsIDMapped()
		if m.IsBind() && !config.RootlessEUID {
			if _, err := os.Stat(m.Source); err != nil {
				wantSourceFile = true
			}
		}
		if wantSourceFile {
			// Request a source file from the host.
			if err := writeSyncArg(pipe, procMountPlease, m); err != nil {
				return fmt.Errorf("failed to request mountfd for %q: %w", m.Source, err)
			}
			sync, err := readSyncFull(pipe, procMountFd)
			if err != nil {
				return fmt.Errorf("mountfd request for %q failed: %w", m.Source, err)
			}
			if sync.File == nil {
				return fmt.Errorf("mountfd request for %q: response missing attached fd", m.Source)
			}
			defer sync.File.Close()
			// Sanity-check to make sure we didn't get the wrong fd back. Note
			// that while m.Source might contain symlinks, the (*os.File).Name
			// is based on the path provided to os.OpenFile, not what it
			// resolves to. So this should never happen.
			if sync.File.Name() != m.Source {
				return fmt.Errorf("returned mountfd for %q doesn't match requested mount configuration: mountfd path is %q", m.Source, sync.File.Name())
			}
			// Unmarshal the procMountFd argument (the file is sync.File).
			var src *mountSource
			if sync.Arg == nil {
				return fmt.Errorf("sync %q is missing an argument", sync.Type)
			}
			if err := json.Unmarshal(*sync.Arg, &src); err != nil {
				return fmt.Errorf("invalid mount fd response argument %q: %w", string(*sync.Arg), err)
			}
			if src == nil {
				return fmt.Errorf("mountfd request for %q: no mount source info received", m.Source)
			}
			src.file = sync.File
			entry.srcFile = src
		}
		if err := mountToRootfs(mountConfig, entry); err != nil {
			return fmt.Errorf("error mounting %q to rootfs at %q: %w", m.Source, m.Destination, err)
		}
	}

	setupDev := needsSetupDev(config)
	if setupDev {
		if err := createDevices(config); err != nil {
			return fmt.Errorf("error creating device nodes: %w", err)
		}
		if err := setupPtmx(config); err != nil {
			return fmt.Errorf("error setting up ptmx: %w", err)
		}
		if err := setupDevSymlinks(config.Rootfs); err != nil {
			return fmt.Errorf("error setting up /dev symlinks: %w", err)
		}
	}

	// Signal the parent to run the pre-start hooks.
	// The hooks are run after the mounts are setup, but before we switch to the new
	// root, so that the old root is still available in the hooks for any mount
	// manipulations.
	// Note that iConfig.Cwd is not guaranteed to exist here.
	if err := syncParentHooks(pipe); err != nil {
		return err
	}

	// The reason these operations are done here rather than in finalizeRootfs
	// is because the console-handling code gets quite sticky if we have to set
	// up the console before doing the pivot_root(2). This is because the
	// Console API has to also work with the ExecIn case, which means that the
	// API must be able to deal with being inside as well as outside the
	// container. It's just cleaner to do this here (at the expense of the
	// operation not being perfectly split).

	if err := unix.Chdir(config.Rootfs); err != nil {
		return &os.PathError{Op: "chdir", Path: config.Rootfs, Err: err}
	}

	if s := iConfig.SpecState; s != nil {
		s.Pid = unix.Getpid()
		s.Status = specs.StateCreating
		if err := iConfig.Config.Hooks.Run(configs.CreateContainer, s); err != nil {
			return err
		}
	}

	if config.NoPivotRoot {
		err = msMoveRoot(config.Rootfs)
	} else if config.Namespaces.Contains(configs.NEWNS) {
		err = pivotRoot(config.Rootfs)
	} else {
		err = chroot()
	}
	if err != nil {
		return fmt.Errorf("error jailing process inside rootfs: %w", err)
	}

	// Apply root mount propagation flags.
	// This must be done after pivot_root/chroot because the mount propagation flag is applied
	// to the current root ("/"), and not to the old rootfs before it becomes "/". Applying the
	// flag in prepareRoot would affect the host mount namespace if the container's
	// root mount is shared.
	// MS_PRIVATE is skipped as rootfsParentMountPrivate() is already called.
	if config.RootPropagation != 0 && config.RootPropagation&unix.MS_PRIVATE == 0 {
		if err := mount("", "/", "", uintptr(config.RootPropagation), ""); err != nil {
			return fmt.Errorf("unable to apply root propagation flags: %w", err)
		}
	}

	if setupDev {
		if err := reOpenDevNull(); err != nil {
			return fmt.Errorf("error reopening /dev/null inside container: %w", err)
		}
	}

	if cwd := iConfig.Cwd; cwd != "" {
		// Note that spec.Process.Cwd can contain unclean value like  "../../../../foo/bar...".
		// However, we are safe to call MkDirAll directly because we are in the jail here.
		if err := os.MkdirAll(cwd, 0o755); err != nil {
			return err
		}
	}

	return nil
}

// finalizeRootfs sets anything to ro if necessary. You must call
// prepareRootfs first.
func finalizeRootfs(config *configs.Config) (err error) {
	// All tmpfs mounts and /dev were previously mounted as rw
	// by mountPropagate. Remount them read-only as requested.
	for _, m := range config.Mounts {
		if m.Flags&unix.MS_RDONLY != unix.MS_RDONLY {
			continue
		}
		if m.Device == "tmpfs" || utils.CleanPath(m.Destination) == "/dev" {
			if err := remountReadonly(m); err != nil {
				return err
			}
		}
	}

	// set rootfs ( / ) as readonly
	if config.Readonlyfs {
		if err := setReadonly(); err != nil {
			return fmt.Errorf("error setting rootfs as readonly: %w", err)
		}
	}

	if config.Umask != nil {
		unix.Umask(int(*config.Umask))
	} else {
		unix.Umask(0o022)
	}
	return nil
}

// /tmp has to be mounted as private to allow MS_MOVE to work in all situations
func prepareTmp(topTmpDir string) (string, error) {
	tmpdir, err := os.MkdirTemp(topTmpDir, "runctop")
	if err != nil {
		return "", err
	}
	if err := mount(tmpdir, tmpdir, "bind", unix.MS_BIND, ""); err != nil {
		return "", err
	}
	if err := mount("", tmpdir, "", uintptr(unix.MS_PRIVATE), ""); err != nil {
		return "", err
	}
	return tmpdir, nil
}

func cleanupTmp(tmpdir string) {
	_ = unix.Unmount(tmpdir, 0)
	_ = os.RemoveAll(tmpdir)
}

func mountCgroupV1(m mountEntry, c *mountConfig) error {
	binds, err := getCgroupMounts(m.Mount)
	if err != nil {
		return err
	}
	var merged []string
	for _, b := range binds {
		ss := filepath.Base(b.Destination)
		if strings.Contains(ss, ",") {
			merged = append(merged, ss)
		}
	}
	tmpfs := &configs.Mount{
		Source:           "tmpfs",
		Device:           "tmpfs",
		Destination:      m.Destination,
		Flags:            defaultMountFlags,
		Data:             "mode=755",
		PropagationFlags: m.PropagationFlags,
	}

	if err := mountToRootfs(c, mountEntry{Mount: tmpfs}); err != nil {
		return err
	}

	for _, b := range binds {
		if c.cgroupns {
			// We just created the tmpfs, and so we can just use filepath.Join
			// here (not to mention we want to make sure we create the path
			// inside the tmpfs, so we don't want to resolve symlinks).
			subsystemPath := filepath.Join(c.root, b.Destination)
			subsystemName := filepath.Base(b.Destination)
			if err := pathrs.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil {
				return err
			}
			if err := utils.WithProcfd(c.root, b.Destination, func(dstFd string) error {
				flags := defaultMountFlags
				if m.Flags&unix.MS_RDONLY != 0 {
					flags = flags | unix.MS_RDONLY
				}
				var (
					source = "cgroup"
					data   = subsystemName
				)
				if data == "systemd" {
					data = cgroups.CgroupNamePrefix + data
					source = "systemd"
				}
				return mountViaFds(source, nil, b.Destination, dstFd, "cgroup", uintptr(flags), data)
			}); err != nil {
				return err
			}
		} else {
			if err := mountToRootfs(c, mountEntry{Mount: b}); err != nil {
				return err
			}
		}
	}
	for _, mc := range merged {
		for _, ss := range strings.Split(mc, ",") {
			// symlink(2) is very dumb, it will just shove the path into
			// the link and doesn't do any checks or relative path
			// conversion. Also, don't error out if the cgroup already exists.
			if err := os.Symlink(mc, filepath.Join(c.root, m.Destination, ss)); err != nil && !os.IsExist(err) {
				return err
			}
		}
	}
	return nil
}

func mountCgroupV2(m mountEntry, c *mountConfig) error {
	err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
		return mountViaFds(m.Source, nil, m.Destination, dstFd, "cgroup2", uintptr(m.Flags), m.Data)
	})
	if err == nil || (!errors.Is(err, unix.EPERM) && !errors.Is(err, unix.EBUSY)) {
		return err
	}

	// When we are in UserNS but CgroupNS is not unshared, we cannot mount
	// cgroup2 (#2158), so fall back to bind mount.
	bindM := &configs.Mount{
		Device:           "bind",
		Source:           fs2.UnifiedMountpoint,
		Destination:      m.Destination,
		Flags:            unix.MS_BIND | m.Flags,
		PropagationFlags: m.PropagationFlags,
	}
	if c.cgroupns && c.cgroup2Path != "" {
		// Emulate cgroupns by bind-mounting the container cgroup path
		// rather than the whole /sys/fs/cgroup.
		bindM.Source = c.cgroup2Path
	}
	// mountToRootfs() handles remounting for MS_RDONLY.
	err = mountToRootfs(c, mountEntry{Mount: bindM})
	if c.rootlessCgroups && errors.Is(err, unix.ENOENT) {
		// ENOENT (for `src = c.cgroup2Path`) happens when rootless runc is being executed
		// outside the userns+mountns.
		//
		// Mask `/sys/fs/cgroup` to ensure it is read-only, even when `/sys` is mounted
		// with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`).
		err = utils.WithProcfdFile(m.dstFile, func(procfd string) error {
			return maskPaths([]string{procfd}, c.label)
		})
	}
	return err
}

func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) {
	// Set up a scratch dir for the tmpfs on the host.
	tmpdir, err := prepareTmp("/tmp")
	if err != nil {
		return fmt.Errorf("tmpcopyup: failed to setup tmpdir: %w", err)
	}
	defer cleanupTmp(tmpdir)
	tmpDir, err := os.MkdirTemp(tmpdir, "runctmpdir")
	if err != nil {
		return fmt.Errorf("tmpcopyup: failed to create tmpdir: %w", err)
	}
	defer os.RemoveAll(tmpDir)

	tmpDirFile, err := os.OpenFile(tmpDir, unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
	if err != nil {
		return fmt.Errorf("tmpcopyup: %w", err)
	}
	defer tmpDirFile.Close()

	// Configure the *host* tmpdir as if it's the container mount. We change
	// m.dstFile since we are going to mount *on the host*.
	hostMount := mountEntry{
		Mount:   m.Mount,
		dstFile: tmpDirFile,
	}
	if err := hostMount.mountPropagate("/", mountLabel); err != nil {
		return err
	}
	defer func() {
		if Err != nil {
			if err := unmount(tmpDir, unix.MNT_DETACH); err != nil {
				logrus.Warnf("tmpcopyup: %v", err)
			}
		}
	}()

	return utils.WithProcfdFile(m.dstFile, func(dstFd string) (Err error) {
		// Copy the container data to the host tmpdir. We append "/" to force
		// CopyDirectory to resolve the symlink rather than trying to copy the
		// symlink itself.
		if err := fileutils.CopyDirectory(dstFd+"/", tmpDir); err != nil {
			return fmt.Errorf("tmpcopyup: failed to copy %s to %s (%s): %w", m.Destination, dstFd, tmpDir, err)
		}
		// Now move the mount into the container.
		if err := mountViaFds(tmpDir, nil, m.Destination, dstFd, "", unix.MS_MOVE, ""); err != nil {
			return fmt.Errorf("tmpcopyup: failed to move mount: %w", err)
		}
		return nil
	})
}

const (
	// The atime "enum" flags (which are mutually exclusive).
	mntAtimeEnumFlags = unix.MS_NOATIME | unix.MS_RELATIME | unix.MS_STRICTATIME
	// All atime-related flags.
	mntAtimeFlags = mntAtimeEnumFlags | unix.MS_NODIRATIME
	// Flags which can be locked when inheriting mounts in a different userns.
	// In the kernel, these are the mounts that are locked using MNT_LOCK_*.
	mntLockFlags = unix.MS_RDONLY | unix.MS_NODEV | unix.MS_NOEXEC |
		unix.MS_NOSUID | mntAtimeFlags
)

func statfsToMountFlags(st unix.Statfs_t) int {
	// From <linux/statfs.h>.
	const ST_NOSYMFOLLOW = 0x2000 //nolint:revive

	var flags int
	for _, f := range []struct {
		st, ms int
	}{
		// See calculate_f_flags() in fs/statfs.c.
		{unix.ST_RDONLY, unix.MS_RDONLY},
		{unix.ST_NOSUID, unix.MS_NOSUID},
		{unix.ST_NODEV, unix.MS_NODEV},
		{unix.ST_NOEXEC, unix.MS_NOEXEC},
		{unix.ST_MANDLOCK, unix.MS_MANDLOCK},
		{unix.ST_SYNCHRONOUS, unix.MS_SYNCHRONOUS},
		{unix.ST_NOATIME, unix.MS_NOATIME},
		{unix.ST_NODIRATIME, unix.MS_NODIRATIME},
		{unix.ST_RELATIME, unix.MS_RELATIME},
		{ST_NOSYMFOLLOW, unix.MS_NOSYMFOLLOW},
		// There is no ST_STRICTATIME -- see below.
	} {
		if int(st.Flags)&f.st == f.st {
			flags |= f.ms
		}
	}
	// MS_STRICTATIME is a "fake" MS_* flag. It isn't stored in mnt->mnt_flags,
	// and so it doesn't show up in statfs(2). If none of the other flags in
	// atime enum are present, the mount is MS_STRICTATIME.
	if flags&mntAtimeEnumFlags == 0 {
		flags |= unix.MS_STRICTATIME
	}
	return flags
}

var errRootfsToFile = errors.New("config tries to change rootfs to file")

func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) {
	unsafePath := utils.StripRoot(rootfs, m.Destination)
	dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH)
	defer func() {
		if dstFile != nil && Err != nil {
			_ = dstFile.Close()
		}
	}()
	if err != nil {
		if !errors.Is(err, unix.ENOENT) {
			return fmt.Errorf("lookup mountpoint target: %w", err)
		}

		// If the mountpoint doesn't already exist, we want to create a mountpoint
		// that makes sense for the source. For file bind-mounts this is an empty
		// file, for everything else it's a directory.
		dstIsFile := false
		if m.Device == "bind" {
			fi, _, err := m.srcStat()
			if err != nil {
				// Error out if the source of a bind mount does not exist as we
				// will be unable to bind anything to it.
				return err
			}
			dstIsFile = !fi.IsDir()
		}

		if dstIsFile {
			dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644)
		} else {
			dstFile, err = pathrs.MkdirAllInRootOpen(rootfs, unsafePath, 0o755)
		}
		if err != nil {
			return fmt.Errorf("make mountpoint %q: %w", m.Destination, err)
		}
	}

	if m.Device == "tmpfs" {
		// If the original target exists, copy the mode for the tmpfs mount.
		stat, err := dstFile.Stat()
		if err != nil {
			return fmt.Errorf("check tmpfs source mode: %w", err)
		}
		dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode()))
		if m.Data != "" {
			dt = dt + "," + m.Data
		}
		m.Data = dt
	}

	dstFullPath, err := procfs.ProcSelfFdReadlink(dstFile)
	if err != nil {
		return fmt.Errorf("get mount destination real path: %w", err)
	}
	if !pathrs.IsLexicallyInRoot(rootfs, dstFullPath) {
		return fmt.Errorf("mountpoint %q is outside of rootfs %q", dstFullPath, rootfs)
	}
	if relPath, err := filepath.Rel(rootfs, dstFullPath); err != nil {
		return fmt.Errorf("get relative path of %q: %w", dstFullPath, err)
	} else if relPath == "." {
		return fmt.Errorf("mountpoint %q is on the top of rootfs %q", dstFullPath, rootfs)
	}
	// TODO: Make checkProcMount use dstFile directly to avoid the need to
	// operate on paths here.
	if err := checkProcMount(rootfs, dstFullPath, *m); err != nil {
		return fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err)
	}
	// Update mountEntry.
	m.dstFile = dstFile
	return nil
}

func mountToRootfs(c *mountConfig, m mountEntry) error {
	rootfs := c.root

	// procfs and sysfs are special because we need to ensure they are actually
	// mounted on a specific path in a container without any funny business.
	switch m.Device {
	case "proc", "sysfs":
		// If the destination already exists and is not a directory, we bail
		// out. This is to avoid mounting through a symlink or similar -- which
		// has been a "fun" attack scenario in the past.
		// TODO: This won't be necessary once we switch to libpathrs and we can
		//       stop all of these symlink-exchange attacks.
		dest := filepath.Clean(m.Destination)
		if !pathrs.IsLexicallyInRoot(rootfs, dest) {
			// Do not use securejoin as it resolves symlinks.
			dest = filepath.Join(rootfs, dest)
		}
		if err := checkProcMount(rootfs, dest, m); err != nil {
			return err
		}
		if fi, err := os.Lstat(dest); err != nil {
			if !os.IsNotExist(err) {
				return err
			}
		} else if !fi.IsDir() {
			return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device)
		}
		dstFile, err := pathrs.MkdirAllInRootOpen(rootfs, dest, 0o755)
		if err != nil {
			return err
		}
		defer dstFile.Close()
		// "proc" and "sys" mounts need special handling (without resolving the
		// destination) to avoid attacks.
		m.dstFile = dstFile
		return m.mountPropagate(rootfs, "")
	}

	mountLabel := c.label
	if err := m.createOpenMountpoint(rootfs); err != nil {
		return fmt.Errorf("create mountpoint for %s mount: %w", m.Destination, err)
	}
	defer func() {
		if m.dstFile != nil {
			_ = m.dstFile.Close()
			m.dstFile = nil
		}
	}()

	switch m.Device {
	case "mqueue":
		if err := m.mountPropagate(rootfs, ""); err != nil {
			return err
		}
		return utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
			return label.SetFileLabel(dstFd, mountLabel)
		})
	case "tmpfs":
		var err error
		if m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP {
			err = doTmpfsCopyUp(m, mountLabel)
		} else {
			err = m.mountPropagate(rootfs, mountLabel)
		}
		return err
	case "bind":
		// open_tree()-related shenanigans are all handled in mountViaFds.
		if err := m.mountPropagate(rootfs, mountLabel); err != nil {
			return err
		}

		// The initial MS_BIND won't change the mount options, we need to do a
		// separate MS_BIND|MS_REMOUNT to apply the mount options. We skip
		// doing this if the user has not specified any mount flags at all
		// (including cleared flags) -- in which case we just keep the original
		// mount flags.
		//
		// Note that the fact we check whether any clearing flags are set is in
		// contrast to mount(8)'s current behaviour, but is what users probably
		// expect. See <https://github.com/util-linux/util-linux/issues/2433>.
		if m.Flags & ^(unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT) != 0 || m.ClearedFlags != 0 {
			if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
				flags := m.Flags | unix.MS_BIND | unix.MS_REMOUNT
				// The runtime-spec says we SHOULD map to the relevant mount(8)
				// behaviour. However, it's not clear whether we want the
				// "mount --bind -o ..." or "mount --bind -o remount,..."
				// behaviour here -- both of which are somewhat broken[1].
				//
				// So, if the user has passed "remount" as a mount option, we
				// implement the "mount --bind -o remount" behaviour, otherwise
				// we implement the spiritual intent of the "mount --bind -o"
				// behaviour, which should match what users expect. Maybe
				// mount(8) will eventually implement this behaviour too..
				//
				// [1]: https://github.com/util-linux/util-linux/issues/2433

				// Initially, we emulate "mount --bind -o ..." where we set
				// only the requested flags (clearing any existing flags). The
				// only difference from mount(8) is that we do this
				// unconditionally, regardless of whether any set-me mount
				// options have been requested.
				//
				// TODO: We are not doing any special handling of the atime
				// flags here, which means that the mount will inherit the old
				// atime flags if the user didn't explicitly request a
				// different set of flags. This also has the mount(8) bug where
				// "nodiratime,norelatime" will result in a
				// "nodiratime,relatime" mount.
				mountErr := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(flags), "")
				if mountErr == nil {
					return nil
				}

				// If the mount failed, the mount may contain locked mount
				// flags. In that case, we emulate "mount --bind -o
				// remount,...", where we take the existing mount flags of the
				// mount and apply the request flags (including clearing flags)
				// on top. The main divergence we have from mount(8) here is
				// that we handle atimes correctly to make sure we error out if
				// we cannot fulfil the requested mount flags.

				st, err := m.srcStatfs()
				if err != nil {
					return err
				}
				srcFlags := statfsToMountFlags(*st)

				logrus.Debugf(
					"working around failure to set vfs flags on bind-mount %s: srcFlags=%s flagsSet=%s flagsClr=%s: %v",
					m.Destination, stringifyMountFlags(srcFlags),
					stringifyMountFlags(m.Flags), stringifyMountFlags(m.ClearedFlags), mountErr)

				// If the user explicitly request one of the locked flags *not*
				// be set, we need to return an error to avoid producing mounts
				// that don't match the user's request.
				if cannotClearFlags := srcFlags & m.ClearedFlags & mntLockFlags; cannotClearFlags != 0 {
					return fmt.Errorf("cannot clear locked flags %s: %w", stringifyMountFlags(cannotClearFlags), mountErr)
				}

				// If an MS_*ATIME flag was requested, it must match the
				// existing one. This handles two separate kernel bugs, and
				// matches the logic of can_change_locked_flags() but without
				// these bugs:
				//
				// * (2.6.30+) Since commit 613cbe3d4870 ("Don't set relatime
				// when noatime is specified"), MS_RELATIME is ignored when
				// MS_NOATIME is set. This means that us inheriting MS_NOATIME
				// from a mount while requesting MS_RELATIME would *silently*
				// produce an MS_NOATIME mount.
				//
				// * (2.6.30+) Since its introduction in commit d0adde574b84
				// ("Add a strictatime mount option"), MS_STRICTATIME has
				// caused any passed MS_RELATIME and MS_NOATIME flags to be
				// ignored which results in us *silently* producing
				// MS_STRICTATIME mounts even if the user requested MS_RELATIME
				// or MS_NOATIME.
				if m.Flags&mntAtimeFlags != 0 && m.Flags&mntAtimeFlags != srcFlags&mntAtimeFlags {
					return fmt.Errorf("cannot change locked atime flags %s: %w", stringifyMountFlags(srcFlags&mntAtimeFlags), mountErr)
				}

				// Retry the mount with the existing lockable mount flags
				// applied.
				flags |= srcFlags & mntLockFlags
				mountErr = mountViaFds("", nil, m.Destination, dstFd, "", uintptr(flags), "")
				if mountErr != nil {
					mountErr = fmt.Errorf("remount with locked flags %s re-applied: %w", stringifyMountFlags(srcFlags&mntLockFlags), mountErr)
				}
				return mountErr
			}); err != nil {
				return fmt.Errorf("failed to set user-requested vfs flags on bind-mount: %w", err)
			}
		}

		if m.Relabel != "" {
			if err := label.Validate(m.Relabel); err != nil {
				return err
			}
			shared := label.IsShared(m.Relabel)
			if err := label.Relabel(m.Source, mountLabel, shared); err != nil {
				return err
			}
		}
		return setRecAttr(m)
	case "cgroup":
		if cgroups.IsCgroup2UnifiedMode() {
			return mountCgroupV2(m, c)
		}
		return mountCgroupV1(m, c)
	default:
		return m.mountPropagate(rootfs, mountLabel)
	}
}

func getCgroupMounts(m *configs.Mount) ([]*configs.Mount, error) {
	mounts, err := cgroups.GetCgroupMounts(false)
	if err != nil {
		return nil, err
	}

	// We don't need to use /proc/thread-self here because runc always runs
	// with every thread in the same cgroup. This lets us avoid having to do
	// runtime.LockOSThread.
	cgroupPaths, err := cgroups.ParseCgroupFile("/proc/self/cgroup")
	if err != nil {
		return nil, err
	}

	var binds []*configs.Mount

	for _, mm := range mounts {
		dir, err := mm.GetOwnCgroup(cgroupPaths)
		if err != nil {
			return nil, err
		}
		relDir, err := filepath.Rel(mm.Root, dir)
		if err != nil {
			return nil, err
		}
		binds = append(binds, &configs.Mount{
			Device:           "bind",
			Source:           filepath.Join(mm.Mountpoint, relDir),
			Destination:      filepath.Join(m.Destination, filepath.Base(mm.Mountpoint)),
			Flags:            unix.MS_BIND | unix.MS_REC | m.Flags,
			PropagationFlags: m.PropagationFlags,
		})
	}

	return binds, nil
}

// Taken from <include/linux/proc_ns.h>. If a file is on a filesystem of type
// PROC_SUPER_MAGIC, we're guaranteed that only the root of the superblock will
// have this inode number.
const procRootIno = 1

// checkProcMount checks to ensure that the mount destination is not over the top of /proc.
// dest is required to be an abs path and have any symlinks resolved before calling this function.
//
// If m is nil, don't stat the filesystem.  This is used for restore of a checkpoint.
func checkProcMount(rootfs, dest string, m mountEntry) error {
	const procPath = "/proc"
	path, err := filepath.Rel(filepath.Join(rootfs, procPath), dest)
	if err != nil {
		return err
	}
	// pass if the mount path is located outside of /proc
	if strings.HasPrefix(path, "..") {
		return nil
	}
	if path == "." {
		// Only allow bind-mounts on top of /proc, and only if the source is a
		// procfs mount.
		if m.IsBind() {
			fsSt, err := m.srcStatfs()
			if err != nil {
				return err
			}
			if fsSt.Type == unix.PROC_SUPER_MAGIC {
				if _, uSt, err := m.srcStat(); err != nil {
					return err
				} else if uSt.Ino != procRootIno {
					// We cannot error out in this case, because we've
					// supported these kinds of mounts for a long time.
					// However, we would expect users to bind-mount the root of
					// a real procfs on top of /proc in the container. We might
					// want to block this in the future.
					logrus.Warnf("bind-mount %v (source %v) is of type procfs but is not the root of a procfs (inode %d). Future versions of runc might block this configuration -- please report an issue to <https://github.com/opencontainers/runc> if you see this warning.", dest, m.srcName(), uSt.Ino)
				}
				return nil
			}
		} else if m.Device == "proc" {
			// Fresh procfs-type mounts are always safe to mount on top of /proc.
			return nil
		}
		return fmt.Errorf("%q cannot be mounted because it is not of type proc", dest)
	}

	// Here dest is definitely under /proc. Do not allow those,
	// except for a few specific entries emulated by lxcfs.
	validProcMounts := []string{
		"/proc/cpuinfo",
		"/proc/diskstats",
		"/proc/meminfo",
		"/proc/stat",
		"/proc/swaps",
		"/proc/uptime",
		"/proc/loadavg",
		"/proc/slabinfo",
		"/proc/sys/kernel/ns_last_pid",
		"/proc/sys/crypto/fips_enabled",
	}
	for _, valid := range validProcMounts {
		path, err := filepath.Rel(filepath.Join(rootfs, valid), dest)
		if err != nil {
			return err
		}
		if path == "." {
			return nil
		}
	}

	return fmt.Errorf("%q cannot be mounted because it is inside /proc", dest)
}

func setupDevSymlinks(rootfs string) error {
	// In theory, these should be links to /proc/thread-self, but systems
	// expect these to be /proc/self and this matches how most distributions
	// work.
	links := [][2]string{
		{"/proc/self/fd", "/dev/fd"},
		{"/proc/self/fd/0", "/dev/stdin"},
		{"/proc/self/fd/1", "/dev/stdout"},
		{"/proc/self/fd/2", "/dev/stderr"},
	}
	// kcore support can be toggled with CONFIG_PROC_KCORE; only create a symlink
	// in /dev if it exists in /proc.
	if _, err := os.Stat("/proc/kcore"); err == nil {
		links = append(links, [2]string{"/proc/kcore", "/dev/core"})
	}
	for _, link := range links {
		var (
			src = link[0]
			dst = filepath.Join(rootfs, link[1])
		)
		if err := os.Symlink(src, dst); err != nil && !os.IsExist(err) {
			return err
		}
	}
	return nil
}

// If stdin, stdout, and/or stderr are pointing to `/dev/null` in the parent's rootfs
// this method will make them point to `/dev/null` in this container's rootfs.  This
// needs to be called after we chroot/pivot into the container's rootfs so that any
// symlinks are resolved locally.
func reOpenDevNull() error {
	file, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
	if err != nil {
		return err
	}
	defer file.Close()
	if err := verifyDevNull(file); err != nil {
		return fmt.Errorf("can't reopen /dev/null: %w", err)
	}
	for fd := 0; fd < 3; fd++ {
		var stat unix.Stat_t
		if err := unix.Fstat(fd, &stat); err != nil {
			return &os.PathError{Op: "fstat", Path: "fd " + strconv.Itoa(fd), Err: err}
		}
		if isDevNull(&stat) {
			// Close and re-open the fd.
			if err := unix.Dup3(int(file.Fd()), fd, 0); err != nil {
				return &os.PathError{
					Op:   "dup3",
					Path: "fd " + strconv.Itoa(int(file.Fd())),
					Err:  err,
				}
			}
		}
	}
	return nil
}

// Create the device nodes in the container.
func createDevices(config *configs.Config) error {
	useBindMount := userns.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER)
	for _, node := range config.Devices {

		// The /dev/ptmx device is setup by setupPtmx()
		if utils.CleanPath(node.Path) == "/dev/ptmx" {
			continue
		}

		// containers running in a user namespace are not allowed to mknod
		// devices so we can just bind mount it from the host.
		if err := createDeviceNode(config.Rootfs, node, useBindMount); err != nil {
			return err
		}
	}
	return nil
}

func bindMountDeviceNode(destDir *os.File, destName string, node *devices.Device) error {
	dstFile, err := utils.Openat(destDir, destName, unix.O_CREAT|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o000)
	if err != nil {
		return fmt.Errorf("create device inode %s: %w", node.Path, err)
	}
	defer dstFile.Close()

	return utils.WithProcfdFile(dstFile, func(dstFd string) error {
		return mountViaFds(node.Path, nil, dstFile.Name(), dstFd, "bind", unix.MS_BIND, "")
	})
}

// Creates the device node in the rootfs of the container.
func createDeviceNode(rootfs string, node *devices.Device, bind bool) error {
	if node.Path == "" {
		// The node only exists for cgroup reasons, ignore it here.
		return nil
	}
	destPath, err := securejoin.SecureJoin(rootfs, node.Path)
	if err != nil {
		return err
	}
	if destPath == rootfs {
		return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile)
	}
	destDirPath, destName := filepath.Split(destPath)
	destDir, err := pathrs.MkdirAllInRootOpen(rootfs, destDirPath, 0o755)
	if err != nil {
		return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err)
	}
	if bind {
		return bindMountDeviceNode(destDir, destName, node)
	}
	if err := mknodDevice(destDir, destName, node); err != nil {
		if errors.Is(err, os.ErrExist) {
			return nil
		} else if errors.Is(err, os.ErrPermission) {
			return bindMountDeviceNode(destDir, destName, node)
		}
		return err
	}
	return nil
}

func mknodDevice(destDir *os.File, destName string, node *devices.Device) error {
	fileMode := node.FileMode
	switch node.Type {
	case devices.BlockDevice:
		fileMode |= unix.S_IFBLK
	case devices.CharDevice:
		fileMode |= unix.S_IFCHR
	case devices.FifoDevice:
		fileMode |= unix.S_IFIFO
	default:
		return fmt.Errorf("%c is not a valid device type for device %s", node.Type, node.Path)
	}
	dev, err := node.Mkdev()
	if err != nil {
		return err
	}
	if err := unix.Mknodat(int(destDir.Fd()), destName, uint32(fileMode), int(dev)); err != nil {
		return &os.PathError{Op: "mknodat", Path: filepath.Join(destDir.Name(), destName), Err: err}
	}

	// Get a handle and verify that it matches the expected inode type and
	// major:minor before we operate on it.
	devFile, err := utils.Openat(destDir, destName, unix.O_NOFOLLOW|unix.O_PATH, 0)
	if err != nil {
		return fmt.Errorf("open new %c device inode %s: %w", node.Type, node.Path, err)
	}
	defer devFile.Close()

	if err := sys.VerifyInode(devFile, func(stat *unix.Stat_t, _ *unix.Statfs_t) error {
		if stat.Mode&unix.S_IFMT != uint32(fileMode)&unix.S_IFMT {
			return fmt.Errorf("new %c device inode %s has incorrect ftype: %#x doesn't match expected %#v",
				node.Type, node.Path,
				stat.Mode&unix.S_IFMT, fileMode&unix.S_IFMT)
		}
		if stat.Rdev != dev {
			return fmt.Errorf("new %c device inode %s has incorrect major:minor: %d:%d doesn't match expected %d:%d",
				node.Type, node.Path,
				unix.Major(stat.Rdev), unix.Minor(stat.Rdev),
				unix.Major(dev), unix.Minor(dev))
		}
		return nil
	}); err != nil {
		return err
	}

	// Ensure permission bits (can be different because of umask).
	if err := sys.FchmodFile(devFile, uint32(fileMode)); err != nil {
		return fmt.Errorf("update new %c device inode %s file mode: %w", node.Type, node.Path, err)
	}
	if err := sys.FchownFile(devFile, int(node.Uid), int(node.Gid)); err != nil {
		return fmt.Errorf("update new %c device inode %s owner: %w", node.Type, node.Path, err)
	}
	runtime.KeepAlive(devFile)
	return nil
}

// rootfsParentMountPrivate ensures rootfs parent mount is private.
// This is needed for two reasons:
//   - pivot_root() will fail if parent mount is shared;
//   - when we bind mount rootfs, if its parent is not private, the new mount
//     will propagate (leak!) to parent namespace and we don't want that.
func rootfsParentMountPrivate(path string) error {
	var err error
	// Assuming path is absolute and clean (this is checked in
	// libcontainer/validate). Any error other than EINVAL means we failed,
	// and EINVAL means this is not a mount point, so traverse up until we
	// find one.
	for {
		err = unix.Mount("", path, "", unix.MS_PRIVATE, "")
		if err == nil {
			return nil
		}
		if err != unix.EINVAL || path == "/" { //nolint:errorlint // unix errors are bare
			break
		}
		path = filepath.Dir(path)
	}
	return &mountError{
		op:     "remount-private",
		target: path,
		flags:  unix.MS_PRIVATE,
		err:    err,
	}
}

func prepareRoot(config *configs.Config) error {
	flag := unix.MS_SLAVE | unix.MS_REC
	if config.RootPropagation != 0 {
		flag = config.RootPropagation
	}
	if err := mount("", "/", "", uintptr(flag), ""); err != nil {
		return err
	}

	if err := rootfsParentMountPrivate(config.Rootfs); err != nil {
		return err
	}

	return mount(config.Rootfs, config.Rootfs, "bind", unix.MS_BIND|unix.MS_REC, "")
}

func setReadonly() error {
	flags := uintptr(unix.MS_BIND | unix.MS_REMOUNT | unix.MS_RDONLY)

	err := mount("", "/", "", flags, "")
	if err == nil {
		return nil
	}
	var s unix.Statfs_t
	if err := unix.Statfs("/", &s); err != nil {
		return &os.PathError{Op: "statfs", Path: "/", Err: err}
	}
	flags |= uintptr(s.Flags)
	return mount("", "/", "", flags, "")
}

func setupPtmx(config *configs.Config) error {
	ptmx := filepath.Join(config.Rootfs, "dev/ptmx")
	if err := os.Remove(ptmx); err != nil && !os.IsNotExist(err) {
		return err
	}
	if err := os.Symlink("pts/ptmx", ptmx); err != nil {
		return err
	}
	return nil
}

// pivotRoot will call pivot_root such that rootfs becomes the new root
// filesystem, and everything else is cleaned up.
func pivotRoot(rootfs string) error {
	// While the documentation may claim otherwise, pivot_root(".", ".") is
	// actually valid. What this results in is / being the new root but
	// /proc/self/cwd being the old root. Since we can play around with the cwd
	// with pivot_root this allows us to pivot without creating directories in
	// the rootfs. Shout-outs to the LXC developers for giving us this idea.

	oldroot, err := unix.Open("/", unix.O_DIRECTORY|unix.O_RDONLY, 0)
	if err != nil {
		return &os.PathError{Op: "open", Path: "/", Err: err}
	}
	defer unix.Close(oldroot) //nolint: errcheck

	newroot, err := unix.Open(rootfs, unix.O_DIRECTORY|unix.O_RDONLY, 0)
	if err != nil {
		return &os.PathError{Op: "open", Path: rootfs, Err: err}
	}
	defer unix.Close(newroot) //nolint: errcheck

	// Change to the new root so that the pivot_root actually acts on it.
	if err := unix.Fchdir(newroot); err != nil {
		return &os.PathError{Op: "fchdir", Path: "fd " + strconv.Itoa(newroot), Err: err}
	}

	if err := unix.PivotRoot(".", "."); err != nil {
		return &os.PathError{Op: "pivot_root", Path: ".", Err: err}
	}

	// Currently our "." is oldroot (according to the current kernel code).
	// However, purely for safety, we will fchdir(oldroot) since there isn't
	// really any guarantee from the kernel what /proc/self/cwd will be after a
	// pivot_root(2).

	if err := unix.Fchdir(oldroot); err != nil {
		return &os.PathError{Op: "fchdir", Path: "fd " + strconv.Itoa(oldroot), Err: err}
	}

	// Make oldroot rslave to make sure our unmounts don't propagate to the
	// host (and thus bork the machine). We don't use rprivate because this is
	// known to cause issues due to races where we still have a reference to a
	// mount while a process in the host namespace are trying to operate on
	// something they think has no mounts (devicemapper in particular).
	if err := mount("", ".", "", unix.MS_SLAVE|unix.MS_REC, ""); err != nil {
		return err
	}
	// Perform the unmount. MNT_DETACH allows us to unmount /proc/self/cwd.
	if err := unmount(".", unix.MNT_DETACH); err != nil {
		return err
	}

	// Switch back to our shiny new root.
	if err := unix.Chdir("/"); err != nil {
		return &os.PathError{Op: "chdir", Path: "/", Err: err}
	}
	return nil
}

func msMoveRoot(rootfs string) error {
	// Before we move the root and chroot we have to mask all "full" sysfs and
	// procfs mounts which exist on the host. This is because while the kernel
	// has protections against mounting procfs if it has masks, when using
	// chroot(2) the *host* procfs mount is still reachable in the mount
	// namespace and the kernel permits procfs mounts inside --no-pivot
	// containers.
	//
	// Users shouldn't be using --no-pivot except in exceptional circumstances,
	// but to avoid such a trivial security flaw we apply a best-effort
	// protection here. The kernel only allows a mount of a pseudo-filesystem
	// like procfs or sysfs if there is a *full* mount (the root of the
	// filesystem is mounted) without any other locked mount points covering a
	// subtree of the mount.
	//
	// So we try to unmount (or mount tmpfs on top of) any mountpoint which is
	// a full mount of either sysfs or procfs (since those are the most
	// concerning filesystems to us).
	mountinfos, err := mountinfo.GetMounts(func(info *mountinfo.Info) (skip, stop bool) {
		// Collect every sysfs and procfs filesystem, except for those which
		// are non-full mounts or are inside the rootfs of the container.
		if info.Root != "/" ||
			(info.FSType != "proc" && info.FSType != "sysfs") ||
			strings.HasPrefix(info.Mountpoint, rootfs) {
			skip = true
		}
		return skip, stop
	})
	if err != nil {
		return err
	}
	for _, info := range mountinfos {
		p := info.Mountpoint
		// Be sure umount events are not propagated to the host.
		if err := mount("", p, "", unix.MS_SLAVE|unix.MS_REC, ""); err != nil {
			if errors.Is(err, unix.ENOENT) {
				// If the mountpoint doesn't exist that means that we've
				// already blasted away some parent directory of the mountpoint
				// and so we don't care about this error.
				continue
			}
			return err
		}
		if err := unmount(p, unix.MNT_DETACH); err != nil {
			if !errors.Is(err, unix.EINVAL) && !errors.Is(err, unix.EPERM) {
				return err
			} else {
				// If we have not privileges for umounting (e.g. rootless), then
				// cover the path.
				if err := mount("tmpfs", p, "tmpfs", 0, ""); err != nil {
					return err
				}
			}
		}
	}

	// Move the rootfs on top of "/" in our mount namespace.
	if err := mount(rootfs, "/", "", unix.MS_MOVE, ""); err != nil {
		return err
	}
	return chroot()
}

func chroot() error {
	if err := unix.Chroot("."); err != nil {
		return &os.PathError{Op: "chroot", Path: ".", Err: err}
	}
	if err := unix.Chdir("/"); err != nil {
		return &os.PathError{Op: "chdir", Path: "/", Err: err}
	}
	return nil
}

// readonlyPath will make a path read only.
func readonlyPath(path string) error {
	if err := mount(path, path, "", unix.MS_BIND|unix.MS_REC, ""); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil
		}
		return err
	}

	var s unix.Statfs_t
	if err := unix.Statfs(path, &s); err != nil {
		return &os.PathError{Op: "statfs", Path: path, Err: err}
	}
	flags := uintptr(s.Flags) & (unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC)

	if err := mount(path, path, "", flags|unix.MS_BIND|unix.MS_REMOUNT|unix.MS_RDONLY, ""); err != nil {
		return err
	}

	return nil
}

// remountReadonly will remount an existing mount point and ensure that it is read-only.
func remountReadonly(m *configs.Mount) error {
	var (
		dest  = m.Destination
		flags = m.Flags
	)
	for i := 0; i < 5; i++ {
		// There is a special case in the kernel for
		// MS_REMOUNT | MS_BIND, which allows us to change only the
		// flags even as an unprivileged user (i.e. user namespace)
		// assuming we don't drop any security related flags (nodev,
		// nosuid, etc.). So, let's use that case so that we can do
		// this re-mount without failing in a userns.
		flags |= unix.MS_REMOUNT | unix.MS_BIND | unix.MS_RDONLY
		if err := mount("", dest, "", uintptr(flags), ""); err != nil {
			if errors.Is(err, unix.EBUSY) {
				time.Sleep(100 * time.Millisecond)
				continue
			}
			return err
		}
		return nil
	}
	return fmt.Errorf("unable to mount %s as readonly max retries reached", dest)
}

func isDevNull(st *unix.Stat_t) bool {
	return st.Mode&unix.S_IFMT == unix.S_IFCHR && st.Rdev == unix.Mkdev(1, 3)
}

func verifyDevNull(f *os.File) error {
	return sys.VerifyInode(f, func(st *unix.Stat_t, _ *unix.Statfs_t) error {
		if !isDevNull(st) {
			return errors.New("container's /dev/null is invalid")
		}
		return nil
	})
}

// maskPaths masks the top of the specified paths inside a container to avoid
// security issues from processes reading information from non-namespace aware
// mounts ( proc/kcore ).
// For files, maskPath bind mounts /dev/null over the top of the specified path.
// For directories, maskPath mounts read-only tmpfs over the top of the specified path.
func maskPaths(paths []string, mountLabel string) error {
	devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0)
	if err != nil {
		return fmt.Errorf("can't mask paths: %w", err)
	}
	defer devNull.Close()
	if err := verifyDevNull(devNull); err != nil {
		return fmt.Errorf("can't mask paths: %w", err)
	}
	devNullSrc := &mountSource{Type: mountSourcePlain, file: devNull}
	procSelfFd, closer := utils.ProcThreadSelf("fd/")
	defer closer()

	for _, path := range paths {
		// Open the target path; skip if it doesn't exist.
		dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				continue
			}
			return fmt.Errorf("can't mask path %q: %w", path, err)
		}
		st, err := dstFh.Stat()
		if err != nil {
			dstFh.Close()
			return fmt.Errorf("can't mask path %q: %w", path, err)
		}
		var dstType string
		if st.IsDir() {
			// Destination is a directory: bind mount a ro tmpfs over it.
			dstType = "dir"
			err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel))
		} else {
			// Destination is a file: mount it to /dev/null.
			dstType = "path"
			dstFd := filepath.Join(procSelfFd, strconv.Itoa(int(dstFh.Fd())))
			err = mountViaFds("", devNullSrc, path, dstFd, "", unix.MS_BIND, "")
		}
		dstFh.Close()
		if err != nil {
			return fmt.Errorf("can't mask %s %q: %w", dstType, path, err)
		}
	}

	return nil
}

func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err error) {
	fullPath, err := procfs.ProcSelfFdReadlink(f)
	if err != nil {
		return nil, fmt.Errorf("get full path: %w", err)
	}
	if !pathrs.IsLexicallyInRoot(rootfs, fullPath) {
		return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs)
	}
	unsafePath := utils.StripRoot(rootfs, fullPath)
	reopened, err := pathrs.OpenInRoot(rootfs, unsafePath, flags)
	if err != nil {
		return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err)
	}
	defer func() {
		if Err != nil {
			_ = reopened.Close()
		}
	}()

	// NOTE: The best we can do here is confirm that the new mountpoint handle
	// matches the original target handle, but an attacker could've swapped a
	// different path to replace it. In the worst case this could result in us
	// applying later vfsmount flags onto the wrong mount.
	//
	// This is far from ideal, but the only way of doing this in a race-free
	// way is to switch the new mount API (move_mount(2) does not require this
	// re-opening step, and thus no such races are possible).
	reopenedFullPath, err := procfs.ProcSelfFdReadlink(reopened)
	if err != nil {
		return nil, fmt.Errorf("check full path of re-opened mountpoint: %w", err)
	}
	if reopenedFullPath != fullPath {
		return nil, fmt.Errorf("mountpoint %q was moved while re-opening", unsafePath)
	}
	return reopened, nil
}

// Do the mount operation followed by additional mounts required to take care
// of propagation flags. This will always be scoped inside the container rootfs.
func (m *mountEntry) mountPropagate(rootfs string, mountLabel string) error {
	var (
		data  = label.FormatMountLabel(m.Data, mountLabel)
		flags = m.Flags
	)
	// Delay mounting the filesystem read-only if we need to do further
	// operations on it. We need to set up files in "/dev", and other tmpfs
	// mounts may need to be chmod-ed after mounting. These mounts will be
	// remounted ro later in finalizeRootfs(), if necessary.
	if m.Device == "tmpfs" || utils.CleanPath(m.Destination) == "/dev" {
		flags &= ^unix.MS_RDONLY
	}

	if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
		return mountViaFds(m.Source, m.srcFile, m.Destination, dstFd, m.Device, uintptr(flags), data)
	}); err != nil {
		return err
	}

	// We need to re-open the mountpoint after doing the mount, in order for us
	// to operate on the new mount we just created. However, we cannot use
	// pathrs.Reopen because we need to re-resolve from the parent directory to
	// get a new handle to the top mount.
	//
	// TODO: Use move_mount(2) on newer kernels so that this is no longer
	// necessary on modern systems.
	newDstFile, err := reopenAfterMount(rootfs, m.dstFile, unix.O_PATH)
	if err != nil {
		return fmt.Errorf("reopen mountpoint after mount: %w", err)
	}
	_ = m.dstFile.Close()
	m.dstFile = newDstFile

	// We have to apply mount propagation flags in a separate WithProcfd() call
	// because the previous call invalidates the passed procfd -- the mount
	// target needs to be re-opened.
	if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
		for _, pflag := range m.PropagationFlags {
			if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		return fmt.Errorf("change mount propagation through procfd: %w", err)
	}
	return nil
}

func setRecAttr(m mountEntry) error {
	if m.RecAttr == nil {
		return nil
	}
	return utils.WithProcfdFile(m.dstFile, func(procfd string) error {
		return unix.MountSetattr(-1, procfd, unix.AT_RECURSIVE, m.RecAttr)
	})
}
