/* * mountpoint.go - Contains all the functionality for finding mountpoints and * using UUIDs to refer to them. Specifically, we can find the mountpoint of a * path, get info about a mountpoint, and find mountpoints with a specific UUID. * * Copyright 2017 Google Inc. * Author: Joe Richey (joerichey@google.com) * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package filesystem import ( "fmt" "io/ioutil" "log" "os" "path/filepath" "sort" "strings" "sync" "github.com/pkg/errors" ) /* #include // setmntent, getmntent, endmntent // The file containing mountpoints info and how we should read it const char* mountpoints_filename = "/proc/mounts"; const char* read_mode = "r"; */ import "C" var ( // These maps hold data about the state of the system's mountpoints. mountsByPath map[string]*Mount mountsByDevice map[string][]*Mount // Used to make the mount functions thread safe mountMutex sync.Mutex // True if the maps have been successfully initialized. mountsInitialized bool // Supported tokens for filesystem links uuidToken = "UUID" // Location to perform UUID lookup uuidDirectory = "/dev/disk/by-uuid" ) // getMountInfo populates the Mount mappings by parsing the filesystem // description file using the getmntent functions. Returns ErrBadLoad if the // Mount mappings cannot be populated. func getMountInfo() error { if mountsInitialized { return nil } // make new maps mountsByPath = make(map[string]*Mount) mountsByDevice = make(map[string][]*Mount) // Load the mount information from mountpoints_filename fileHandle := C.setmntent(C.mountpoints_filename, C.read_mode) if fileHandle == nil { return errors.Wrapf(ErrGlobalMountInfo, "could not read %q", C.GoString(C.mountpoints_filename)) } defer C.endmntent(fileHandle) for { entry := C.getmntent(fileHandle) // When getmntent returns nil, we have read all of the entries. if entry == nil { mountsInitialized = true return nil } // Create the Mount structure by converting types. mnt := Mount{ Path: C.GoString(entry.mnt_dir), FilesystemType: C.GoString(entry.mnt_type), Options: strings.Split(C.GoString(entry.mnt_opts), ","), } // Skip invalid mountpoints var err error if mnt.Path, err = canonicalizePath(mnt.Path); err != nil { log.Printf("getting mnt_dir: %v", err) continue } // We can only use mountpoints that are directories for fscrypt. if !isDir(mnt.Path) { log.Printf("mnt_dir %v: not a directory", mnt.Path) continue } // Note this overrides the info if we have seen the mountpoint // earlier in the file. This is correct behavior because the // filesystems are listed in mount order. mountsByPath[mnt.Path] = &mnt deviceName, err := canonicalizePath(C.GoString(entry.mnt_fsname)) // Only use real valid devices (unlike cgroups, tmpfs, ...) if err == nil && isDevice(deviceName) { mnt.Device = deviceName mountsByDevice[deviceName] = append(mountsByDevice[deviceName], &mnt) } } } // AllFilesystems lists all the Mounts on the current system ordered by path. // Use CheckSetup() to see if they are used with fscrypt. func AllFilesystems() ([]*Mount, error) { mountMutex.Lock() defer mountMutex.Unlock() if err := getMountInfo(); err != nil { return nil, err } mounts := make([]*Mount, 0, len(mountsByPath)) for _, mount := range mountsByPath { mounts = append(mounts, mount) } sort.Sort(PathSorter(mounts)) return mounts, nil } // UpdateMountInfo updates the filesystem mountpoint maps with the current state // of the filesystem mountpoints. Returns error if the initialization fails. func UpdateMountInfo() error { mountMutex.Lock() defer mountMutex.Unlock() mountsInitialized = false return getMountInfo() } // FindMount returns the corresponding Mount object for some path in a // filesystem. Note that in the case of a bind mounts there may be two Mount // objects for the same underlying filesystem. An error is returned if the path // is invalid or we cannot load the required mount data. If a filesystem has // been updated since the last call to one of the mount functions, run // UpdateMountInfo to see changes. func FindMount(path string) (*Mount, error) { path, err := canonicalizePath(path) if err != nil { return nil, err } mountMutex.Lock() defer mountMutex.Unlock() if err = getMountInfo(); err != nil { return nil, err } // Traverse up the directory tree until we find a mountpoint for { if mnt, ok := mountsByPath[path]; ok { return mnt, nil } // Move to the parent directory unless we have reached the root. parent := filepath.Dir(path) if parent == path { return nil, errors.Wrap(ErrNotAMountpoint, path) } path = parent } } // GetMount returns the Mount object with a matching mountpoint. An error is // returned if the path is invalid or we cannot load the required mount data. If // a filesystem has been updated since the last call to one of the mount // functions, run UpdateMountInfo to see changes. func GetMount(mountpoint string) (*Mount, error) { mountpoint, err := canonicalizePath(mountpoint) if err != nil { return nil, err } mountMutex.Lock() defer mountMutex.Unlock() if err = getMountInfo(); err != nil { return nil, err } if mnt, ok := mountsByPath[mountpoint]; ok { return mnt, nil } return nil, errors.Wrap(ErrNotAMountpoint, mountpoint) } // getMountsFromLink returns the Mount objects which match the provided link. // This link is formatted as a tag (e.g. =) similar to how they // appear in "/etc/fstab". Currently, only "UUID" tokens are supported. Note // that this can match multiple Mounts (due to the existence of bind mounts). An // error is returned if the link is invalid or we cannot load the required mount // data. If a filesystem has been updated since the last call to one of the // mount functions, run UpdateMountInfo to see the change. func getMountsFromLink(link string) ([]*Mount, error) { // Parse the link linkComponents := strings.Split(link, "=") if len(linkComponents) != 2 { return nil, errors.Wrapf(ErrFollowLink, "link %q format is invalid", link) } token := linkComponents[0] value := linkComponents[1] if token != uuidToken { return nil, errors.Wrapf(ErrFollowLink, "token type %q not supported", token) } // See if UUID points to an existing device searchPath := filepath.Join(uuidDirectory, value) if filepath.Base(searchPath) != value { return nil, errors.Wrapf(ErrFollowLink, "value %q is not a UUID", value) } devicePath, err := canonicalizePath(searchPath) if err != nil { return nil, errors.Wrapf(ErrFollowLink, "no device with UUID %q", value) } // Lookup mountpoints for device in global store mountMutex.Lock() defer mountMutex.Unlock() if err := getMountInfo(); err != nil { return nil, err } mnts, ok := mountsByDevice[devicePath] if !ok { return nil, errors.Wrapf(ErrFollowLink, "no mounts for device %q", devicePath) } return mnts, nil } // makeLink returns a link of the form = where value is the tag // value for the Mount's device. Currently, only "UUID" tokens are supported. An // error is returned if the mount has no device, or no UUID. func makeLink(mnt *Mount, token string) (string, error) { if token != uuidToken { return "", errors.Wrapf(ErrMakeLink, "token type %q not supported", token) } if mnt.Device == "" { return "", errors.Wrapf(ErrMakeLink, "no device for mount %q", mnt.Path) } dirContents, err := ioutil.ReadDir(uuidDirectory) if err != nil { return "", errors.Wrap(ErrMakeLink, err.Error()) } for _, fileInfo := range dirContents { if fileInfo.Mode()&os.ModeSymlink == 0 { continue // Only interested in UUID symlinks } uuid := fileInfo.Name() devicePath, err := canonicalizePath(filepath.Join(uuidDirectory, uuid)) if err != nil { log.Print(err) continue } if mnt.Device == devicePath { return fmt.Sprintf("%s=%s", uuidToken, uuid), nil } } return "", errors.Wrapf(ErrMakeLink, "device %q has no UUID", mnt.Device) }