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 }