github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/mountinfo/mountinfo_linux.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  // Copyright (c) 2015-2021 MinIO, Inc.
     5  //
     6  // This file is part of MinIO Object Storage stack
     7  //
     8  // This program is free software: you can redistribute it and/or modify
     9  // it under the terms of the GNU Affero General Public License as published by
    10  // the Free Software Foundation, either version 3 of the License, or
    11  // (at your option) any later version.
    12  //
    13  // This program is distributed in the hope that it will be useful
    14  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    15  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    16  // GNU Affero General Public License for more details.
    17  //
    18  // You should have received a copy of the GNU Affero General Public License
    19  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    20  
    21  package mountinfo
    22  
    23  import (
    24  	"bufio"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"path/filepath"
    29  	"strconv"
    30  	"strings"
    31  	"syscall"
    32  )
    33  
    34  const (
    35  	// Number of fields per line in /proc/mounts as per the fstab man page.
    36  	expectedNumFieldsPerLine = 6
    37  	// Location of the mount file to use
    38  	procMountsPath = "/proc/mounts"
    39  )
    40  
    41  // IsLikelyMountPoint determines if a directory is a mountpoint.
    42  func IsLikelyMountPoint(path string) bool {
    43  	s1, err := os.Lstat(path)
    44  	if err != nil {
    45  		return false
    46  	}
    47  
    48  	// A symlink can never be a mount point
    49  	if s1.Mode()&os.ModeSymlink != 0 {
    50  		return false
    51  	}
    52  
    53  	s2, err := os.Lstat(filepath.Dir(strings.TrimSuffix(path, "/")))
    54  	if err != nil {
    55  		return false
    56  	}
    57  
    58  	// If the directory has a different device as parent, then it is a mountpoint.
    59  	if s1.Sys().(*syscall.Stat_t).Dev != s2.Sys().(*syscall.Stat_t).Dev {
    60  		//  path/.. on a different device as path
    61  		return true
    62  	}
    63  
    64  	// path/.. is the same i-node as path - this check is for bind mounts.
    65  	return s1.Sys().(*syscall.Stat_t).Ino == s2.Sys().(*syscall.Stat_t).Ino
    66  }
    67  
    68  // CheckCrossDevice - check if any list of paths has any sub-mounts at /proc/mounts.
    69  func CheckCrossDevice(absPaths []string) error {
    70  	return checkCrossDevice(absPaths, procMountsPath)
    71  }
    72  
    73  // Check cross device is an internal function.
    74  func checkCrossDevice(absPaths []string, mountsPath string) error {
    75  	mounts, err := readProcMounts(mountsPath)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	for _, path := range absPaths {
    80  		if err := mounts.checkCrossMounts(path); err != nil {
    81  			return err
    82  		}
    83  	}
    84  	return nil
    85  }
    86  
    87  // CheckCrossDevice - check if given path has any sub-mounts in the input mounts list.
    88  func (mts mountInfos) checkCrossMounts(path string) error {
    89  	if !filepath.IsAbs(path) {
    90  		return fmt.Errorf("Invalid argument, path (%s) is expected to be absolute", path)
    91  	}
    92  	var crossMounts mountInfos
    93  	for _, mount := range mts {
    94  		// Add a separator to indicate that this is a proper mount-point.
    95  		// This is to avoid a situation where prefix is '/tmp/fsmount'
    96  		// and mount path is /tmp/fs. In such a scenario we need to check for
    97  		// `/tmp/fs/` to be a common prefix amount other mounts.
    98  		mpath := strings.TrimSuffix(mount.Path, "/") + "/"
    99  		ppath := strings.TrimSuffix(path, "/") + "/"
   100  		if strings.HasPrefix(mpath, ppath) {
   101  			// At this point if the mount point has a common prefix two conditions can happen.
   102  			// - mount.Path matches exact with `path` means we can proceed no error here.
   103  			// - mount.Path doesn't match (means cross-device mount), should error out.
   104  			if mount.Path != path {
   105  				crossMounts = append(crossMounts, mount)
   106  			}
   107  		}
   108  	}
   109  	msg := `Cross-device mounts detected on path (%s) at following locations %s. Export path should not have any sub-mounts, refusing to start.`
   110  	if len(crossMounts) > 0 {
   111  		// if paths didn't match then we do have cross-device mount.
   112  		return fmt.Errorf(msg, path, crossMounts)
   113  	}
   114  	return nil
   115  }
   116  
   117  // readProcMounts reads the given mountFilePath (normally /proc/mounts) and produces a hash
   118  // of the contents.  If the out argument is not nil, this fills it with MountPoint structs.
   119  func readProcMounts(mountFilePath string) (mountInfos, error) {
   120  	file, err := os.Open(mountFilePath)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	defer file.Close()
   125  	return parseMountFrom(file)
   126  }
   127  
   128  func parseMountFrom(file io.Reader) (mountInfos, error) {
   129  	mounts := mountInfos{}
   130  	scanner := bufio.NewReader(file)
   131  	for {
   132  		line, err := scanner.ReadString('\n')
   133  		if err == io.EOF {
   134  			break
   135  		}
   136  
   137  		fields := strings.Fields(line)
   138  		if len(fields) != expectedNumFieldsPerLine {
   139  			// ignore incorrect lines.
   140  			continue
   141  		}
   142  
   143  		// Freq should be an integer.
   144  		if _, err := strconv.Atoi(fields[4]); err != nil {
   145  			return nil, err
   146  		}
   147  
   148  		// Pass should be an integer.
   149  		if _, err := strconv.Atoi(fields[5]); err != nil {
   150  			return nil, err
   151  		}
   152  
   153  		mounts = append(mounts, mountInfo{
   154  			Device:  fields[0],
   155  			Path:    fields[1],
   156  			FSType:  fields[2],
   157  			Options: strings.Split(fields[3], ","),
   158  			Freq:    fields[4],
   159  			Pass:    fields[5],
   160  		})
   161  	}
   162  	return mounts, nil
   163  }