github.com/kaydxh/golang@v0.0.131/go/filesystem/mountpoint.go (about)

     1  /*
     2   *Copyright (c) 2022, kaydxh
     3   *
     4   *Permission is hereby granted, free of charge, to any person obtaining a copy
     5   *of this software and associated documentation files (the "Software"), to deal
     6   *in the Software without restriction, including without limitation the rights
     7   *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     8   *copies of the Software, and to permit persons to whom the Software is
     9   *furnished to do so, subject to the following conditions:
    10   *
    11   *The above copyright notice and this permission notice shall be included in all
    12   *copies or substantial portions of the Software.
    13   *
    14   *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    15   *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    16   *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    17   *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    18   *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    19   *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    20   *SOFTWARE.
    21   */
    22  package filesystem
    23  
    24  import (
    25  	"bufio"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"path/filepath"
    30  	"strconv"
    31  	"strings"
    32  	"sync"
    33  
    34  	os_ "github.com/kaydxh/golang/go/os"
    35  	filepath_ "github.com/kaydxh/golang/go/path/filepath"
    36  
    37  	"github.com/pkg/errors"
    38  	"github.com/sirupsen/logrus"
    39  	"golang.org/x/sys/unix"
    40  )
    41  
    42  var (
    43  	mountsByDevice    map[DeviceNumber]*Mount
    44  	mountsByPath      map[string]*Mount
    45  	mountMutex        sync.Mutex
    46  	mountsInitialized bool
    47  	allMountsByDevice map[DeviceNumber][]*Mount
    48  )
    49  
    50  type DeviceNumber uint64
    51  
    52  func (num DeviceNumber) String() string {
    53  	return fmt.Sprintf("%d:%d", unix.Major(uint64(num)), unix.Minor(uint64(num)))
    54  }
    55  
    56  // getNumberOfContainingDevice returns the device number of the filesystem which
    57  // contains the given file.  If the file is a symlink, it is not dereferenced.
    58  func getNumberOfContainingDevice(path string) (DeviceNumber, error) {
    59  	var stat unix.Stat_t
    60  	if err := unix.Lstat(path, &stat); err != nil {
    61  		return 0, err
    62  	}
    63  	return DeviceNumber(stat.Dev), nil
    64  }
    65  
    66  func filesystemLacksMainMountError(deviceNumber DeviceNumber) error {
    67  	return errors.Errorf(
    68  		"Device %q (%v) lacks a \"main\" mountpoint in the current mount namespace, so it's ambiguous where to store the fscrypt metadata.",
    69  		getDeviceName(deviceNumber),
    70  		deviceNumber,
    71  	)
    72  }
    73  
    74  type Mount struct {
    75  	Path           string
    76  	FilesystemType string
    77  	Device         string
    78  	DeviceNumber   DeviceNumber
    79  	Subtree        string
    80  	ReadOnly       bool
    81  }
    82  
    83  func FindMount(path string) (*Mount, error) {
    84  	mountMutex.Lock()
    85  	defer mountMutex.Unlock()
    86  	if err := loadMountInfo(); err != nil {
    87  		return nil, err
    88  	}
    89  	// First try to find the mount by the number of the containing device.
    90  	deviceNumber, err := getNumberOfContainingDevice(path)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	mnt, ok := mountsByDevice[deviceNumber]
    95  
    96  	if ok {
    97  		if mnt == nil {
    98  			mnts, ok := allMountsByDevice[deviceNumber]
    99  			if ok {
   100  				if len(mnts) == 0 {
   101  					return nil, filesystemLacksMainMountError(deviceNumber)
   102  				}
   103  
   104  				for _, mnt := range mnts {
   105  					if strings.HasPrefix(path, mnt.Path) {
   106  						return mnt, nil
   107  					}
   108  				}
   109  			}
   110  			return nil, filesystemLacksMainMountError(deviceNumber)
   111  		}
   112  
   113  		return mnt, nil
   114  	}
   115  	// The mount couldn't be found by the number of the containing device.
   116  	// Fall back to walking up the directory hierarchy and checking for a
   117  	// mount at each directory path.  This is necessary for btrfs, where
   118  	// files report a different st_dev from the /proc/self/mountinfo entry.
   119  	curPath, err := filepath_.CanonicalizePath(path)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	for {
   124  		mnt := mountsByPath[curPath]
   125  		if mnt != nil {
   126  			return mnt, nil
   127  		}
   128  		// Move to the parent directory unless we have reached the root.
   129  		parent := filepath.Dir(curPath)
   130  		if parent == curPath {
   131  			return nil, errors.Errorf("couldn't find mountpoint containing %q", path)
   132  		}
   133  		curPath = parent
   134  	}
   135  }
   136  
   137  // loadMountInfo populates the Mount mappings by parsing /proc/self/mountinfo.
   138  // It returns an error if the Mount mappings cannot be populated.
   139  func loadMountInfo() error {
   140  	if !mountsInitialized {
   141  		file, err := os.Open("/proc/self/mountinfo")
   142  		if err != nil {
   143  			return err
   144  		}
   145  		defer file.Close()
   146  		if err := readMountInfo(file); err != nil {
   147  			return err
   148  		}
   149  		mountsInitialized = true
   150  	}
   151  	return nil
   152  }
   153  
   154  // This is separate from loadMountInfo() only for unit testing.
   155  func readMountInfo(r io.Reader) error {
   156  	mountsByDevice = make(map[DeviceNumber]*Mount)
   157  	mountsByPath = make(map[string]*Mount)
   158  	allMountsByDevice = make(map[DeviceNumber][]*Mount)
   159  	allMountsByPath := make(map[string]*Mount)
   160  
   161  	scanner := bufio.NewScanner(r)
   162  	for scanner.Scan() {
   163  		line := scanner.Text()
   164  		mnt := parseMountInfoLine(line)
   165  		if mnt == nil {
   166  			logrus.Warnf("ignoring invalid mountinfo line %q", line)
   167  			continue
   168  		}
   169  
   170  		// We can only use mountpoints that are directories for fscrypt.
   171  		isDir, err := os_.IsDir(mnt.Path)
   172  		if err != nil {
   173  			logrus.Errorf("ignoring mountpoint %v because isDir failed", err)
   174  			continue
   175  		}
   176  
   177  		if !isDir {
   178  			logrus.Infof("ignoring mountpoint %q because it is not a directory", mnt.Path)
   179  			continue
   180  		}
   181  
   182  		// Note this overrides the info if we have seen the mountpoint
   183  		// earlier in the file. This is correct behavior because the
   184  		// mountpoints are listed in mount order.
   185  		allMountsByPath[mnt.Path] = mnt
   186  	}
   187  	// For each filesystem, choose a "main" Mount and discard any additional
   188  	// bind mounts.  fscrypt only cares about the main Mount, since it's
   189  	// where the fscrypt metadata is stored.  Store all the main Mounts in
   190  	// mountsByDevice and mountsByPath so that they can be found later.
   191  	for _, mnt := range allMountsByPath {
   192  		allMountsByDevice[mnt.DeviceNumber] =
   193  			append(allMountsByDevice[mnt.DeviceNumber], mnt)
   194  	}
   195  
   196  	for deviceNumber, filesystemMounts := range allMountsByDevice {
   197  		mnt := findMainMount(filesystemMounts)
   198  		mountsByDevice[deviceNumber] = mnt // may store an explicit nil entry
   199  		if mnt != nil {
   200  			mountsByPath[mnt.Path] = mnt
   201  		}
   202  	}
   203  	return nil
   204  }
   205  
   206  // For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt
   207  func parseMountInfoLine(line string) *Mount {
   208  	fields := strings.Split(line, " ")
   209  	if len(fields) < 10 {
   210  		return nil
   211  	}
   212  
   213  	// Count the optional fields.  In case new fields are appended later,
   214  	// don't simply assume that n == len(fields) - 4.
   215  	n := 6
   216  	for fields[n] != "-" {
   217  		n++
   218  		if n >= len(fields) {
   219  			return nil
   220  		}
   221  	}
   222  	if n+3 >= len(fields) {
   223  		return nil
   224  	}
   225  
   226  	var mnt *Mount = &Mount{}
   227  	var err error
   228  	mnt.DeviceNumber, err = newDeviceNumberFromString(fields[2])
   229  	if err != nil {
   230  		return nil
   231  	}
   232  	mnt.Subtree = unescapeString(fields[3])
   233  	mnt.Path = unescapeString(fields[4])
   234  	for _, opt := range strings.Split(fields[5], ",") {
   235  		if opt == "ro" {
   236  			mnt.ReadOnly = true
   237  		}
   238  	}
   239  	mnt.FilesystemType = unescapeString(fields[n+1])
   240  	mnt.Device = getDeviceName(mnt.DeviceNumber)
   241  	return mnt
   242  }
   243  
   244  func newDeviceNumberFromString(str string) (DeviceNumber, error) {
   245  	var major, minor uint32
   246  	if count, _ := fmt.Sscanf(str, "%d:%d", &major, &minor); count != 2 {
   247  		return 0, errors.Errorf("invalid device number string %q", str)
   248  	}
   249  	return DeviceNumber(unix.Mkdev(major, minor)), nil
   250  }
   251  
   252  // Unescape octal-encoded escape sequences in a string from the mountinfo file.
   253  // The kernel encodes the ' ', '\t', '\n', and '\\' bytes this way.  This
   254  // function exactly inverts what the kernel does, including by preserving
   255  // invalid UTF-8.
   256  func unescapeString(str string) string {
   257  	var sb strings.Builder
   258  	for i := 0; i < len(str); i++ {
   259  		b := str[i]
   260  		if b == '\\' && i+3 < len(str) {
   261  			if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil {
   262  				b = uint8(parsed)
   263  				i += 3
   264  			}
   265  		}
   266  		sb.WriteByte(b)
   267  	}
   268  	return sb.String()
   269  }
   270  
   271  // We get the device name via the device number rather than use the mount source
   272  // field directly.  This is necessary to handle a rootfs that was mounted via
   273  // the kernel command line, since mountinfo always shows /dev/root for that.
   274  // This assumes that the device nodes are in the standard location.
   275  func getDeviceName(num DeviceNumber) string {
   276  	linkPath := fmt.Sprintf("/sys/dev/block/%v", num)
   277  	if target, err := os.Readlink(linkPath); err == nil {
   278  		return fmt.Sprintf("/dev/%s", filepath.Base(target))
   279  	}
   280  	return ""
   281  }
   282  
   283  type mountpointTreeNode struct {
   284  	mount    *Mount
   285  	parent   *mountpointTreeNode
   286  	children []*mountpointTreeNode
   287  }
   288  
   289  func addUncontainedSubtreesRecursive(dst map[string]bool,
   290  	node *mountpointTreeNode, allUncontainedSubtrees map[string]bool) {
   291  	if allUncontainedSubtrees[node.mount.Subtree] {
   292  		dst[node.mount.Subtree] = true
   293  	}
   294  	for _, child := range node.children {
   295  		addUncontainedSubtreesRecursive(dst, child, allUncontainedSubtrees)
   296  	}
   297  }
   298  
   299  func findMainMount(filesystemMounts []*Mount) *Mount {
   300  	// Index this filesystem's mounts by path.  Note: paths are unique here,
   301  	// since non-last mounts were already excluded earlier.
   302  	//
   303  	// Also build the set of all mounted subtrees.
   304  	filesystemMountsByPath := make(map[string]*mountpointTreeNode)
   305  	allSubtrees := make(map[string]bool)
   306  	for _, mnt := range filesystemMounts {
   307  		filesystemMountsByPath[mnt.Path] = &mountpointTreeNode{mount: mnt}
   308  		allSubtrees[mnt.Subtree] = true
   309  	}
   310  
   311  	// Divide the mounts into non-overlapping trees of mountpoints.
   312  	for path, mntNode := range filesystemMountsByPath {
   313  		for path != "/" && mntNode.parent == nil {
   314  			path = filepath.Dir(path)
   315  			if parent := filesystemMountsByPath[path]; parent != nil {
   316  				mntNode.parent = parent
   317  				parent.children = append(parent.children, mntNode)
   318  			}
   319  		}
   320  	}
   321  
   322  	// Build the set of mounted subtrees that aren't contained in any other
   323  	// mounted subtree.
   324  	allUncontainedSubtrees := make(map[string]bool)
   325  	for subtree := range allSubtrees {
   326  		contained := false
   327  		for t := subtree; t != "/" && !contained; {
   328  			t = filepath.Dir(t)
   329  			contained = allSubtrees[t]
   330  		}
   331  		if !contained {
   332  			allUncontainedSubtrees[subtree] = true
   333  		}
   334  	}
   335  
   336  	// Select the root of a mountpoint tree whose mounted subtrees contain
   337  	// *all* mounted subtrees.  Equivalently, select a mountpoint tree in
   338  	// which every uncontained subtree is mounted.
   339  	var mainMount *Mount
   340  	for _, mntNode := range filesystemMountsByPath {
   341  		mnt := mntNode.mount
   342  		if mntNode.parent != nil {
   343  			continue
   344  		}
   345  		uncontainedSubtrees := make(map[string]bool)
   346  		addUncontainedSubtreesRecursive(uncontainedSubtrees, mntNode, allUncontainedSubtrees)
   347  		if len(uncontainedSubtrees) != len(allUncontainedSubtrees) {
   348  			continue
   349  		}
   350  		// If there's more than one eligible mount, they should have the
   351  		// same Subtree.  Otherwise it's ambiguous which one to use.
   352  		if mainMount != nil && mainMount.Subtree != mnt.Subtree {
   353  			logrus.Errorf(
   354  				"Unsupported case: %q (%v) has multiple non-overlapping mounts. This filesystem will be ignored!",
   355  				mnt.Device,
   356  				mnt.DeviceNumber,
   357  			)
   358  			return nil
   359  		}
   360  		// Prefer a read-write mount to a read-only one.
   361  		if mainMount == nil || mainMount.ReadOnly {
   362  			mainMount = mnt
   363  		}
   364  	}
   365  	return mainMount
   366  }