github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/mount_linux.go (about)

     1  package libcontainer
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/fs"
     7  	"os"
     8  	"strconv"
     9  
    10  	"github.com/sirupsen/logrus"
    11  	"golang.org/x/sys/unix"
    12  
    13  	"github.com/opencontainers/runc/libcontainer/configs"
    14  	"github.com/opencontainers/runc/libcontainer/userns"
    15  	"github.com/opencontainers/runc/libcontainer/utils"
    16  )
    17  
    18  // mountSourceType indicates what type of file descriptor is being returned. It
    19  // is used to tell rootfs_linux.go whether or not to use move_mount(2) to
    20  // install the mount.
    21  type mountSourceType string
    22  
    23  const (
    24  	// An open_tree(2)-style file descriptor that needs to be installed using
    25  	// move_mount(2) to install.
    26  	mountSourceOpenTree mountSourceType = "open_tree"
    27  	// A plain file descriptor that can be mounted through /proc/thread-self/fd.
    28  	mountSourcePlain mountSourceType = "plain-open"
    29  )
    30  
    31  type mountSource struct {
    32  	Type mountSourceType `json:"type"`
    33  	file *os.File        `json:"-"`
    34  }
    35  
    36  // mountError holds an error from a failed mount or unmount operation.
    37  type mountError struct {
    38  	op      string
    39  	source  string
    40  	srcFile *mountSource
    41  	target  string
    42  	dstFd   string
    43  	flags   uintptr
    44  	data    string
    45  	err     error
    46  }
    47  
    48  // Error provides a string error representation.
    49  func (e *mountError) Error() string {
    50  	out := e.op + " "
    51  
    52  	if e.source != "" {
    53  		out += "src=" + e.source + ", "
    54  		if e.srcFile != nil {
    55  			out += "srcType=" + string(e.srcFile.Type) + ", "
    56  			out += "srcFd=" + strconv.Itoa(int(e.srcFile.file.Fd())) + ", "
    57  		}
    58  	}
    59  	out += "dst=" + e.target
    60  	if e.dstFd != "" {
    61  		out += ", dstFd=" + e.dstFd
    62  	}
    63  
    64  	if e.flags != uintptr(0) {
    65  		out += ", flags=0x" + strconv.FormatUint(uint64(e.flags), 16)
    66  	}
    67  	if e.data != "" {
    68  		out += ", data=" + e.data
    69  	}
    70  
    71  	out += ": " + e.err.Error()
    72  	return out
    73  }
    74  
    75  // Unwrap returns the underlying error.
    76  // This is a convention used by Go 1.13+ standard library.
    77  func (e *mountError) Unwrap() error {
    78  	return e.err
    79  }
    80  
    81  // mount is a simple unix.Mount wrapper, returning an error with more context
    82  // in case it failed.
    83  func mount(source, target, fstype string, flags uintptr, data string) error {
    84  	return mountViaFds(source, nil, target, "", fstype, flags, data)
    85  }
    86  
    87  // mountViaFds is a unix.Mount wrapper which uses srcFile instead of source,
    88  // and dstFd instead of target, unless those are empty.
    89  //
    90  // If srcFile is non-nil and flags does not contain MS_REMOUNT, mountViaFds
    91  // will mount it according to the mountSourceType of the file descriptor.
    92  //
    93  // The dstFd argument, if non-empty, is expected to be in the form of a path to
    94  // an opened file descriptor on procfs (i.e. "/proc/thread-self/fd/NN").
    95  //
    96  // If a file descriptor is used instead of a source or a target path, the
    97  // corresponding path is only used to add context to an error in case the mount
    98  // operation has failed.
    99  func mountViaFds(source string, srcFile *mountSource, target, dstFd, fstype string, flags uintptr, data string) error {
   100  	// MS_REMOUNT and srcFile don't make sense together.
   101  	if srcFile != nil && flags&unix.MS_REMOUNT != 0 {
   102  		logrus.Debugf("mount source passed along with MS_REMOUNT -- ignoring srcFile")
   103  		srcFile = nil
   104  	}
   105  	dst := target
   106  	if dstFd != "" {
   107  		dst = dstFd
   108  	}
   109  	src := source
   110  	isMoveMount := srcFile != nil && srcFile.Type == mountSourceOpenTree
   111  	if srcFile != nil {
   112  		// If we're going to use the /proc/thread-self/... path for classic
   113  		// mount(2), we need to get a safe handle to /proc/thread-self. This
   114  		// isn't needed for move_mount(2) because in that case the path is just
   115  		// a dummy string used for error info.
   116  		srcFileFd := srcFile.file.Fd()
   117  		if isMoveMount {
   118  			src = "/proc/self/fd/" + strconv.Itoa(int(srcFileFd))
   119  		} else {
   120  			var closer utils.ProcThreadSelfCloser
   121  			src, closer = utils.ProcThreadSelfFd(srcFileFd)
   122  			defer closer()
   123  		}
   124  	}
   125  
   126  	var op string
   127  	var err error
   128  	if isMoveMount {
   129  		op = "move_mount"
   130  		err = unix.MoveMount(int(srcFile.file.Fd()), "",
   131  			unix.AT_FDCWD, dstFd,
   132  			unix.MOVE_MOUNT_F_EMPTY_PATH|unix.MOVE_MOUNT_T_SYMLINKS)
   133  	} else {
   134  		op = "mount"
   135  		err = unix.Mount(src, dst, fstype, flags, data)
   136  	}
   137  	if err != nil {
   138  		return &mountError{
   139  			op:      op,
   140  			source:  source,
   141  			srcFile: srcFile,
   142  			target:  target,
   143  			dstFd:   dstFd,
   144  			flags:   flags,
   145  			data:    data,
   146  			err:     err,
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  // unmount is a simple unix.Unmount wrapper.
   153  func unmount(target string, flags int) error {
   154  	err := unix.Unmount(target, flags)
   155  	if err != nil {
   156  		return &mountError{
   157  			op:     "unmount",
   158  			target: target,
   159  			flags:  uintptr(flags),
   160  			err:    err,
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  // syscallMode returns the syscall-specific mode bits from Go's portable mode bits.
   167  // Copy from https://cs.opensource.google/go/go/+/refs/tags/go1.20.7:src/os/file_posix.go;l=61-75
   168  func syscallMode(i fs.FileMode) (o uint32) {
   169  	o |= uint32(i.Perm())
   170  	if i&fs.ModeSetuid != 0 {
   171  		o |= unix.S_ISUID
   172  	}
   173  	if i&fs.ModeSetgid != 0 {
   174  		o |= unix.S_ISGID
   175  	}
   176  	if i&fs.ModeSticky != 0 {
   177  		o |= unix.S_ISVTX
   178  	}
   179  	// No mapping for Go's ModeTemporary (plan9 only).
   180  	return
   181  }
   182  
   183  // mountFd creates a "mount source fd" (either through open_tree(2) or just
   184  // open(O_PATH)) based on the provided configuration. This function must be
   185  // called from within the container's mount namespace.
   186  //
   187  // In the case of idmapped mount configurations, the returned mount source will
   188  // be an open_tree(2) file with MOUNT_ATTR_IDMAP applied. For other
   189  // bind-mounts, it will be an O_PATH. If the type of mount cannot be handled,
   190  // the returned mountSource will be nil, indicating that the container init
   191  // process will need to do an old-fashioned mount(2) themselves.
   192  //
   193  // This helper is only intended to be used by goCreateMountSources.
   194  func mountFd(nsHandles *userns.Handles, m *configs.Mount) (*mountSource, error) {
   195  	if !m.IsBind() {
   196  		return nil, errors.New("new mount api: only bind-mounts are supported")
   197  	}
   198  	if nsHandles == nil {
   199  		nsHandles = new(userns.Handles)
   200  		defer nsHandles.Release()
   201  	}
   202  
   203  	var mountFile *os.File
   204  	var sourceType mountSourceType
   205  
   206  	// Ideally, we would use OPEN_TREE_CLONE for everything, because we can
   207  	// be sure that the file descriptor cannot be used to escape outside of
   208  	// the mount root. Unfortunately, OPEN_TREE_CLONE is far more expensive
   209  	// than open(2) because it requires doing mounts inside a new anonymous
   210  	// mount namespace. So we use open(2) for standard bind-mounts, and
   211  	// OPEN_TREE_CLONE when we need to set mount attributes here.
   212  	//
   213  	// While passing open(2)'d paths from the host rootfs isn't exactly the
   214  	// safest thing in the world, the files will not survive across
   215  	// execve(2) and "runc init" is non-dumpable so it should not be
   216  	// possible for a malicious container process to gain access to the
   217  	// file descriptors. We also don't do any of this for "runc exec",
   218  	// lessening the risk even further.
   219  	if m.IsIDMapped() {
   220  		flags := uint(unix.OPEN_TREE_CLONE | unix.OPEN_TREE_CLOEXEC)
   221  		if m.Flags&unix.MS_REC == unix.MS_REC {
   222  			flags |= unix.AT_RECURSIVE
   223  		}
   224  		fd, err := unix.OpenTree(unix.AT_FDCWD, m.Source, flags)
   225  		if err != nil {
   226  			return nil, &os.PathError{Op: "open_tree(OPEN_TREE_CLONE)", Path: m.Source, Err: err}
   227  		}
   228  		mountFile = os.NewFile(uintptr(fd), m.Source)
   229  		sourceType = mountSourceOpenTree
   230  
   231  		// Configure the id mapping.
   232  		var usernsFile *os.File
   233  		if m.IDMapping.UserNSPath == "" {
   234  			usernsFile, err = nsHandles.Get(userns.Mapping{
   235  				UIDMappings: m.IDMapping.UIDMappings,
   236  				GIDMappings: m.IDMapping.GIDMappings,
   237  			})
   238  			if err != nil {
   239  				return nil, fmt.Errorf("failed to create userns for %s id-mapping: %w", m.Source, err)
   240  			}
   241  		} else {
   242  			usernsFile, err = os.Open(m.IDMapping.UserNSPath)
   243  			if err != nil {
   244  				return nil, fmt.Errorf("failed to open existing userns for %s id-mapping: %w", m.Source, err)
   245  			}
   246  		}
   247  		defer usernsFile.Close()
   248  
   249  		setAttrFlags := uint(unix.AT_EMPTY_PATH)
   250  		// If the mount has "ridmap" set, we apply the configuration
   251  		// recursively. This allows you to create "rbind" mounts where only
   252  		// the top-level mount has an idmapping. I'm not sure why you'd
   253  		// want that, but still...
   254  		if m.IDMapping.Recursive {
   255  			setAttrFlags |= unix.AT_RECURSIVE
   256  		}
   257  		if err := unix.MountSetattr(int(mountFile.Fd()), "", setAttrFlags, &unix.MountAttr{
   258  			Attr_set:  unix.MOUNT_ATTR_IDMAP,
   259  			Userns_fd: uint64(usernsFile.Fd()),
   260  		}); err != nil {
   261  			extraMsg := ""
   262  			if err == unix.EINVAL {
   263  				extraMsg = " (maybe the filesystem used doesn't support idmap mounts on this kernel?)"
   264  			}
   265  
   266  			return nil, fmt.Errorf("failed to set MOUNT_ATTR_IDMAP on %s: %w%s", m.Source, err, extraMsg)
   267  		}
   268  	} else {
   269  		var err error
   270  		mountFile, err = os.OpenFile(m.Source, unix.O_PATH|unix.O_CLOEXEC, 0)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  		sourceType = mountSourcePlain
   275  	}
   276  	return &mountSource{
   277  		Type: sourceType,
   278  		file: mountFile,
   279  	}, nil
   280  }