diff options
Diffstat (limited to 'filesystem')
| -rw-r--r-- | filesystem/filesystem.go | 958 | ||||
| -rw-r--r-- | filesystem/filesystem_test.go | 371 | ||||
| -rw-r--r-- | filesystem/mountpoint.go | 596 | ||||
| -rw-r--r-- | filesystem/mountpoint_test.go | 525 | ||||
| -rw-r--r-- | filesystem/path.go | 83 | ||||
| -rw-r--r-- | filesystem/path_test.go | 85 |
6 files changed, 2242 insertions, 376 deletions
diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go index 86c168a..9829435 100644 --- a/filesystem/filesystem.go +++ b/filesystem/filesystem.go @@ -1,5 +1,5 @@ /* - * filesystem.go - Contains the a functionality for a specific filesystem. This + * filesystem.go - Contains the functionality for a specific filesystem. This * includes the commands to setup the filesystem, apply policies, and locate * metadata. * @@ -21,60 +21,199 @@ // Package filesystem deals with the structure of the files on disk used to // store the metadata for fscrypt. Specifically, this package includes: -// - mountpoint management (mountpoint.go) -// - querying existing mounted filesystems -// - getting filesystems from a UUID -// - finding the filesystem for a specific path -// - metadata organization (filesystem.go) -// - setting up a mounted filesystem for use with fscrypt -// - adding/querying/deleting metadata -// - making links to other filesystems' metadata -// - following links to get data from other filesystems +// 1. mountpoint management (mountpoint.go) +// - querying existing mounted filesystems +// - getting filesystems from a UUID +// - finding the filesystem for a specific path +// 2. metadata organization (filesystem.go) +// - setting up a mounted filesystem for use with fscrypt +// - adding/querying/deleting metadata +// - making links to other filesystems' metadata +// - following links to get data from other filesystems package filesystem import ( "fmt" - "io/ioutil" + "io" "log" "os" + "os/user" "path/filepath" + "sort" "strings" + "syscall" + "time" - "github.com/golang/protobuf/proto" "github.com/pkg/errors" "golang.org/x/sys/unix" + "google.golang.org/protobuf/proto" "github.com/google/fscrypt/metadata" "github.com/google/fscrypt/util" ) -// Filesystem error values -var ( - ErrNotAMountpoint = errors.New("not a mountpoint") - ErrAlreadySetup = errors.New("already setup for use with fscrypt") - ErrNotSetup = errors.New("not setup for use with fscrypt") - ErrNoMetadata = errors.New("could not find metadata") - ErrLinkedProtector = errors.New("not a regular protector") - ErrInvalidMetadata = errors.New("provided metadata is invalid") - ErrFollowLink = errors.New("cannot follow filesystem link") - ErrLinkExpired = errors.New("no longer exists on linked filesystem") - ErrMakeLink = util.SystemError("cannot create filesystem link") - ErrGlobalMountInfo = util.SystemError("creating global mountpoint list failed") - ErrCorruptMetadata = util.SystemError("on-disk metadata is corrupt") -) +// ErrAlreadySetup indicates that a filesystem is already setup for fscrypt. +type ErrAlreadySetup struct { + Mount *Mount +} + +func (err *ErrAlreadySetup) Error() string { + return fmt.Sprintf("filesystem %s is already setup for use with fscrypt", + err.Mount.Path) +} + +// ErrCorruptMetadata indicates that an fscrypt metadata file is corrupt. +type ErrCorruptMetadata struct { + Path string + UnderlyingError error +} + +func (err *ErrCorruptMetadata) Error() string { + return fmt.Sprintf("fscrypt metadata file at %q is corrupt: %s", + err.Path, err.UnderlyingError) +} + +// ErrFollowLink indicates that a protector link can't be followed. +type ErrFollowLink struct { + Link string + UnderlyingError error +} + +func (err *ErrFollowLink) Error() string { + return fmt.Sprintf("cannot follow filesystem link %q: %s", + err.Link, err.UnderlyingError) +} + +// ErrInsecurePermissions indicates that a filesystem is not considered to be +// setup for fscrypt because a metadata directory has insecure permissions. +type ErrInsecurePermissions struct { + Path string +} + +func (err *ErrInsecurePermissions) Error() string { + return fmt.Sprintf("%q has insecure permissions (world-writable without sticky bit)", + err.Path) +} + +// ErrMakeLink indicates that a protector link can't be created. +type ErrMakeLink struct { + Target *Mount + UnderlyingError error +} + +func (err *ErrMakeLink) Error() string { + return fmt.Sprintf("cannot create filesystem link to %q: %s", + err.Target.Path, err.UnderlyingError) +} + +// ErrMountOwnedByAnotherUser indicates that the mountpoint root directory is +// owned by a user that isn't trusted in the current context, so we don't +// consider fscrypt to be properly setup on the filesystem. +type ErrMountOwnedByAnotherUser struct { + Mount *Mount +} + +func (err *ErrMountOwnedByAnotherUser) Error() string { + return fmt.Sprintf("another non-root user owns the root directory of %s", err.Mount.Path) +} + +// ErrNoCreatePermission indicates that the current user lacks permission to +// create fscrypt metadata on the given filesystem. +type ErrNoCreatePermission struct { + Mount *Mount +} + +func (err *ErrNoCreatePermission) Error() string { + return fmt.Sprintf("user lacks permission to create fscrypt metadata on %s", err.Mount.Path) +} + +// ErrNotAMountpoint indicates that a path is not a mountpoint. +type ErrNotAMountpoint struct { + Path string +} + +func (err *ErrNotAMountpoint) Error() string { + return fmt.Sprintf("%q is not a mountpoint", err.Path) +} + +// ErrNotSetup indicates that a filesystem is not setup for fscrypt. +type ErrNotSetup struct { + Mount *Mount +} + +func (err *ErrNotSetup) Error() string { + return fmt.Sprintf("filesystem %s is not setup for use with fscrypt", err.Mount.Path) +} + +// ErrSetupByAnotherUser indicates that one or more of the fscrypt metadata +// directories is owned by a user that isn't trusted in the current context, so +// we don't consider fscrypt to be properly setup on the filesystem. +type ErrSetupByAnotherUser struct { + Mount *Mount +} + +func (err *ErrSetupByAnotherUser) Error() string { + return fmt.Sprintf("another non-root user owns fscrypt metadata directories on %s", err.Mount.Path) +} + +// ErrSetupNotSupported indicates that the given filesystem type is not +// supported for fscrypt setup. +type ErrSetupNotSupported struct { + Mount *Mount +} + +func (err *ErrSetupNotSupported) Error() string { + return fmt.Sprintf("filesystem type %s is not supported for fscrypt setup", + err.Mount.FilesystemType) +} + +// ErrPolicyNotFound indicates that the policy metadata was not found. +type ErrPolicyNotFound struct { + Descriptor string + Mount *Mount +} + +func (err *ErrPolicyNotFound) Error() string { + return fmt.Sprintf("policy metadata for %s not found on filesystem %s", + err.Descriptor, err.Mount.Path) +} + +// ErrProtectorNotFound indicates that the protector metadata was not found. +type ErrProtectorNotFound struct { + Descriptor string + Mount *Mount +} + +func (err *ErrProtectorNotFound) Error() string { + return fmt.Sprintf("protector metadata for %s not found on filesystem %s", + err.Descriptor, err.Mount.Path) +} + +// SortDescriptorsByLastMtime indicates whether descriptors are sorted by last +// modification time when being listed. This can be set to true to get +// consistent output for testing. +var SortDescriptorsByLastMtime = false // Mount contains information for a specific mounted filesystem. -// Path - Absolute path where the directory is mounted -// Filesystem - Name of the mounted filesystem -// Options - List of options used when mounting the filesystem -// Device - Device for filesystem (empty string if we cannot find one) +// +// Path - Absolute path where the directory is mounted +// FilesystemType - Type of the mounted filesystem, e.g. "ext4" +// Device - Device for filesystem (empty string if we cannot find one) +// DeviceNumber - Device number of the filesystem. This is set even if +// Device isn't, since all filesystems have a device +// number assigned by the kernel, even pseudo-filesystems. +// Subtree - The mounted subtree of the filesystem. This is usually +// "/", meaning that the entire filesystem is mounted, but +// it can differ for bind mounts. +// ReadOnly - True if this is a read-only mount // // In order to use a Mount to store fscrypt metadata, some directories must be // setup first. Specifically, the directories created look like: // <mountpoint> // └── .fscrypt -// ├── policies -// └── protectors +// +// ├── policies +// └── protectors // // These "policies" and "protectors" directories will contain files that are // the corresponding metadata structures for policies and protectors. The public @@ -83,13 +222,18 @@ var ( // // There is also the ability to reference another filesystem's metadata. This is // used when a Policy on filesystem A is protected with Protector on filesystem -// B. In this scenario, we store a "link file" in the protectors directory whose -// contents look like "UUID=3a6d9a76-47f0-4f13-81bf-3332fbe984fb". +// B. In this scenario, we store a "link file" in the protectors directory. +// +// We also allow ".fscrypt" to be a symlink which was previously created. This +// allows login protectors to be created when the root filesystem is read-only, +// provided that "/.fscrypt" is a symlink pointing to a writable location. type Mount struct { - Path string - Filesystem string - Options []string - Device string + Path string + FilesystemType string + Device string + DeviceNumber DeviceNumber + Subtree string + ReadOnly bool } // PathSorter allows mounts to be sorted by Path. @@ -109,24 +253,55 @@ const ( // The base directory should be read-only (except for the creator) basePermissions = 0755 - // The subdirectories should be writable to everyone, but they have the - // sticky bit set so users cannot delete other users' metadata. - dirPermissions = os.ModeSticky | 0777 - // The metadata files are globally visible, but can only be deleted by - // the user that created them - filePermissions = 0644 + + // The metadata files shouldn't be readable or writable by other users. + // Having them be world-readable wouldn't necessarily be a huge issue, + // but given that some of these files contain (strong) password hashes, + // we error on the side of caution -- similar to /etc/shadow. + // Note: existing files on-disk might have mode 0644, as that was the + // mode used by fscrypt v0.3.2 and earlier. + filePermissions = os.FileMode(0600) + + // Maximum size of a metadata file. This value is arbitrary, and it can + // be changed. We just set a reasonable limit that shouldn't be reached + // in practice, except by users trying to cause havoc by creating + // extremely large files in the metadata directories. + maxMetadataFileSize = 16384 +) + +// SetupMode is a mode for creating the fscrypt metadata directories. +type SetupMode int + +const ( + // SingleUserWritable specifies to make the fscrypt metadata directories + // writable by a single user (usually root) only. + SingleUserWritable SetupMode = iota + // WorldWritable specifies to make the fscrypt metadata directories + // world-writable (with the sticky bit set). + WorldWritable ) func (m *Mount) String() string { return fmt.Sprintf(`%s - Filsystem: %s - Options: %v - Device: %s`, m.Path, m.Filesystem, m.Options, m.Device) + FilesystemType: %s + Device: %s`, m.Path, m.FilesystemType, m.Device) } -// BaseDir returns the path of the base fscrypt directory on this filesystem. +// BaseDir returns the path to the base fscrypt directory for this filesystem. func (m *Mount) BaseDir() string { - return filepath.Join(m.Path, baseDirName) + rawBaseDir := filepath.Join(m.Path, baseDirName) + // We allow the base directory to be a symlink, but some callers need + // the real path, so dereference the symlink here if needed. Since the + // directory the symlink points to may not exist yet, we have to read + // the symlink manually rather than use filepath.EvalSymlinks. + target, err := os.Readlink(rawBaseDir) + if err != nil { + return rawBaseDir // not a symlink + } + if filepath.IsAbs(target) { + return target + } + return filepath.Join(m.Path, target) } // ProtectorDir returns the directory containing the protector metadata. @@ -151,47 +326,177 @@ func (m *Mount) PolicyDir() string { return filepath.Join(m.BaseDir(), policyDirName) } -// policyPath returns the full path to a regular policy file with the +// PolicyPath returns the full path to a regular policy file with the // specified descriptor. -func (m *Mount) policyPath(descriptor string) string { +func (m *Mount) PolicyPath(descriptor string) string { return filepath.Join(m.PolicyDir(), descriptor) } -// tempMount creates a temporary Mount under the main directory. The path for -// the returned tempMount should be removed by the caller. +// tempMount creates a temporary directory alongside this Mount's base fscrypt +// directory and returns a temporary Mount which represents this temporary +// directory. The caller is responsible for removing this temporary directory. func (m *Mount) tempMount() (*Mount, error) { - trashDir, err := ioutil.TempDir(m.Path, tempPrefix) - return &Mount{Path: trashDir}, err + tempDir, err := os.MkdirTemp(filepath.Dir(m.BaseDir()), tempPrefix) + return &Mount{Path: tempDir}, err +} + +// ErrEncryptionNotEnabled indicates that encryption is not enabled on the given +// filesystem. +type ErrEncryptionNotEnabled struct { + Mount *Mount +} + +func (err *ErrEncryptionNotEnabled) Error() string { + return fmt.Sprintf("encryption not enabled on filesystem %s (%s).", + err.Mount.Path, err.Mount.Device) +} + +// ErrEncryptionNotSupported indicates that encryption is not supported on the +// given filesystem. +type ErrEncryptionNotSupported struct { + Mount *Mount +} + +func (err *ErrEncryptionNotSupported) Error() string { + return fmt.Sprintf("This kernel doesn't support encryption on %s filesystems.", + err.Mount.FilesystemType) } -// err modifies an error to contain the path of this filesystem. -func (m *Mount) err(err error) error { - return errors.Wrapf(err, "filesystem %s", m.Path) +// EncryptionSupportError adds filesystem-specific context to the +// ErrEncryptionNotEnabled and ErrEncryptionNotSupported errors from the +// metadata package. +func (m *Mount) EncryptionSupportError(err error) error { + switch err { + case metadata.ErrEncryptionNotEnabled: + return &ErrEncryptionNotEnabled{m} + case metadata.ErrEncryptionNotSupported: + return &ErrEncryptionNotSupported{m} + } + return err +} + +// isFscryptSetupAllowed decides whether the given filesystem is allowed to be +// set up for fscrypt, without actually accessing it. This basically checks +// whether the filesystem type is one of the types that supports encryption, or +// at least is in some stage of planning for encrption support in the future. +// +// We need this list so that we can skip filesystems that are irrelevant for +// fscrypt without having to look for the fscrypt metadata directories on them, +// which can trigger errors, long delays, or side effects on some filesystems. +// +// Unfortunately, this means that if a completely new filesystem adds encryption +// support, then it will need to be manually added to this list. But it seems +// to be a worthwhile tradeoff to avoid the above issues. +func (m *Mount) isFscryptSetupAllowed() bool { + if m.Path == "/" { + // The root filesystem is always allowed, since it's where login + // protectors are stored. + return true + } + switch m.FilesystemType { + case "ext4", "f2fs", "ubifs", "btrfs", "ceph", "xfs", "lustre": + return true + default: + return false + } } -// CheckSupport returns an error if this filesystem does not support filesystem -// encryption. +// CheckSupport returns an error if this filesystem does not support encryption. func (m *Mount) CheckSupport() error { - return m.err(metadata.CheckSupport(m.Path)) + if !m.isFscryptSetupAllowed() { + return &ErrEncryptionNotSupported{m} + } + return m.EncryptionSupportError(metadata.CheckSupport(m.Path)) +} + +func checkOwnership(path string, info os.FileInfo, trustedUser *user.User) bool { + if trustedUser == nil { + return true + } + trustedUID := uint32(util.AtoiOrPanic(trustedUser.Uid)) + actualUID := info.Sys().(*syscall.Stat_t).Uid + if actualUID != 0 && actualUID != trustedUID { + log.Printf("WARNING: %q is owned by uid %d, but expected %d or 0", + path, actualUID, trustedUID) + return false + } + return true } -// CheckSetup returns an error if all the fscrypt metadata directories do not +// CheckSetup returns an error if any of the fscrypt metadata directories do not // exist. Will log any unexpected errors or incorrect permissions. -func (m *Mount) CheckSetup() error { - // Run all the checks so we will always get all the warnings - baseGood := isDirCheckPerm(m.BaseDir(), basePermissions) - policyGood := isDirCheckPerm(m.PolicyDir(), dirPermissions) - protectorGood := isDirCheckPerm(m.ProtectorDir(), dirPermissions) +func (m *Mount) CheckSetup(trustedUser *user.User) error { + if !m.isFscryptSetupAllowed() { + return &ErrNotSetup{m} + } + // Check that the mountpoint directory itself is not a symlink and has + // proper ownership, as otherwise we can't trust anything beneath it. + info, err := loggedLstat(m.Path) + if err != nil { + return &ErrNotSetup{m} + } + if (info.Mode() & os.ModeSymlink) != 0 { + log.Printf("mountpoint directory %q cannot be a symlink", m.Path) + return &ErrNotSetup{m} + } + if !info.IsDir() { + log.Printf("mountpoint %q is not a directory", m.Path) + return &ErrNotSetup{m} + } + if !checkOwnership(m.Path, info, trustedUser) { + return &ErrMountOwnedByAnotherUser{m} + } - if baseGood && policyGood && protectorGood { - return nil + // Check BaseDir similarly. However, unlike the other directories, we + // allow BaseDir to be a symlink, to support the use case of metadata + // for a read-only filesystem being redirected to a writable location. + info, err = loggedStat(m.BaseDir()) + if err != nil { + return &ErrNotSetup{m} + } + if !info.IsDir() { + log.Printf("%q is not a directory", m.BaseDir()) + return &ErrNotSetup{m} + } + if !checkOwnership(m.Path, info, trustedUser) { + return &ErrMountOwnedByAnotherUser{m} + } + + // Check that the policies and protectors directories aren't symlinks and + // have proper ownership. + subdirs := []string{m.PolicyDir(), m.ProtectorDir()} + for _, path := range subdirs { + info, err := loggedLstat(path) + if err != nil { + return &ErrNotSetup{m} + } + if (info.Mode() & os.ModeSymlink) != 0 { + log.Printf("directory %q cannot be a symlink", path) + return &ErrNotSetup{m} + } + if !info.IsDir() { + log.Printf("%q is not a directory", path) + return &ErrNotSetup{m} + } + // We are no longer too picky about the mode, given that + // 'fscrypt setup' now offers a choice of two different modes, + // and system administrators could customize it further. + // However, we can at least verify that if the directory is + // world-writable, then the sticky bit is also set. + if info.Mode()&(os.ModeSticky|0002) == 0002 { + log.Printf("%q is world-writable but doesn't have sticky bit set", path) + return &ErrInsecurePermissions{path} + } + if !checkOwnership(path, info, trustedUser) { + return &ErrSetupByAnotherUser{m} + } } - return m.err(ErrNotSetup) + return nil } // makeDirectories creates the three metadata directories with the correct // permissions. Note that this function overrides the umask. -func (m *Mount) makeDirectories() error { +func (m *Mount) makeDirectories(setupMode SetupMode) error { // Zero the umask so we get the permissions we want oldMask := unix.Umask(0) defer func() { @@ -201,83 +506,189 @@ func (m *Mount) makeDirectories() error { if err := os.Mkdir(m.BaseDir(), basePermissions); err != nil { return err } - if err := os.Mkdir(m.PolicyDir(), dirPermissions); err != nil { + + var dirMode os.FileMode + switch setupMode { + case SingleUserWritable: + dirMode = 0755 + case WorldWritable: + dirMode = os.ModeSticky | 0777 + } + if err := os.Mkdir(m.PolicyDir(), dirMode); err != nil { return err } - return os.Mkdir(m.ProtectorDir(), dirPermissions) + return os.Mkdir(m.ProtectorDir(), dirMode) +} + +// GetSetupMode returns the current mode for fscrypt metadata creation on this +// filesystem. +func (m *Mount) GetSetupMode() (SetupMode, *user.User, error) { + info1, err1 := os.Stat(m.PolicyDir()) + info2, err2 := os.Stat(m.ProtectorDir()) + + if err1 == nil && err2 == nil { + mask := os.ModeSticky | 0777 + mode1 := info1.Mode() & mask + mode2 := info2.Mode() & mask + uid1 := info1.Sys().(*syscall.Stat_t).Uid + uid2 := info2.Sys().(*syscall.Stat_t).Uid + user, err := util.UserFromUID(int64(uid1)) + if err == nil && mode1 == mode2 && uid1 == uid2 { + switch mode1 { + case mask: + return WorldWritable, nil, nil + case 0755: + return SingleUserWritable, user, nil + } + } + log.Printf("filesystem %s uses custom permissions on metadata directories", m.Path) + } + return -1, nil, errors.New("unable to determine setup mode") } -// Setup sets up the filesystem for use with fscrypt, note that this merely +// Setup sets up the filesystem for use with fscrypt. Note that this merely // creates the appropriate files on the filesystem. It does not actually modify -// the filesystem's feature flags. This operation is atomic, it either succeeds +// the filesystem's feature flags. This operation is atomic; it either succeeds // or no files in the baseDir are created. -func (m *Mount) Setup() error { - if m.CheckSetup() == nil { - return m.err(ErrAlreadySetup) +func (m *Mount) Setup(mode SetupMode) error { + if m.CheckSetup(nil) == nil { + return &ErrAlreadySetup{m} + } + if !m.isFscryptSetupAllowed() { + return &ErrSetupNotSupported{m} } // We build the directories under a temp Mount and then move into place. temp, err := m.tempMount() if err != nil { - return m.err(err) + return err } defer os.RemoveAll(temp.Path) - if err = temp.makeDirectories(); err != nil { - return m.err(err) + if err = temp.makeDirectories(mode); err != nil { + return err } // Atomically move directory into place. - return m.err(os.Rename(temp.BaseDir(), m.BaseDir())) + return os.Rename(temp.BaseDir(), m.BaseDir()) } // RemoveAllMetadata removes all the policy and protector metadata from the -// filesystem. This operation is atomic, it either succeeds or no files in the +// filesystem. This operation is atomic; it either succeeds or no files in the // baseDir are removed. // WARNING: Will cause data loss if the metadata is used to encrypt // directories (this could include directories on other filesystems). func (m *Mount) RemoveAllMetadata() error { - if err := m.CheckSetup(); err != nil { + if err := m.CheckSetup(nil); err != nil { return err } // temp will hold the old metadata temporarily temp, err := m.tempMount() if err != nil { - return m.err(err) + return err } defer os.RemoveAll(temp.Path) // Move directory into temp (to be destroyed on defer) - return m.err(os.Rename(m.BaseDir(), temp.BaseDir())) + return os.Rename(m.BaseDir(), temp.BaseDir()) +} + +func syncDirectory(dirPath string) error { + dirFile, err := os.Open(dirPath) + if err != nil { + return err + } + if err = dirFile.Sync(); err != nil { + dirFile.Close() + return err + } + return dirFile.Close() +} + +func (m *Mount) overwriteDataNonAtomic(path string, data []byte) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|unix.O_NOFOLLOW, 0) + if err != nil { + return err + } + if _, err = file.Write(data); err != nil { + log.Printf("WARNING: overwrite of %q failed; file will be corrupted!", path) + file.Close() + return err + } + if err = file.Sync(); err != nil { + file.Close() + return err + } + if err = file.Close(); err != nil { + return err + } + log.Printf("successfully overwrote %q non-atomically", path) + return nil } -// writeDataAtomic writes the data to the path such that the data is either -// written to stable storage or an error is returned. -func (m *Mount) writeDataAtomic(path string, data []byte) error { - // Write the file to a temporary file then move into place so that the - // operation will be atomic. - tempPath := filepath.Join(filepath.Dir(path), tempPrefix+filepath.Base(path)) - tempFile, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE, filePermissions) +// writeData writes the given data to the given path such that, if possible, the +// data is either written to stable storage or an error is returned. If a file +// already exists at the path, it will be replaced. +// +// However, if the process doesn't have write permission to the directory but +// does have write permission to the file itself, then as a fallback the file is +// overwritten in-place rather than replaced. Note that this may be non-atomic. +func (m *Mount) writeData(path string, data []byte, owner *user.User, mode os.FileMode) error { + // Write the data to a temporary file, sync it, then rename into place + // so that the operation will be atomic. + dirPath := filepath.Dir(path) + tempFile, err := os.CreateTemp(dirPath, tempPrefix) if err != nil { + log.Print(err) + if os.IsPermission(err) { + if _, err = os.Lstat(path); err == nil { + log.Printf("trying non-atomic overwrite of %q", path) + return m.overwriteDataNonAtomic(path, data) + } + return &ErrNoCreatePermission{m} + } return err } - defer os.Remove(tempPath) + defer os.Remove(tempFile.Name()) + // Ensure the new file has the right permissions mask. + if err = tempFile.Chmod(mode); err != nil { + tempFile.Close() + return err + } + // Override the file owner if one was specified. This happens when root + // needs to create files owned by a particular user. + if owner != nil { + if err = util.Chown(tempFile, owner); err != nil { + log.Printf("could not set owner of %q to %v: %v", + path, owner.Username, err) + tempFile.Close() + return err + } + } if _, err = tempFile.Write(data); err != nil { tempFile.Close() return err } + if err = tempFile.Sync(); err != nil { + tempFile.Close() + return err + } if err = tempFile.Close(); err != nil { return err } - return os.Rename(tempPath, path) + if err = os.Rename(tempFile.Name(), path); err != nil { + return err + } + // Ensure the rename has been persisted before returning success. + return syncDirectory(dirPath) } // addMetadata writes the metadata structure to the file with the specified -// path this will overwrite any existing data. The operation is atomic. -func (m *Mount) addMetadata(path string, md metadata.Metadata) error { +// path. This will overwrite any existing data. The operation is atomic. +func (m *Mount) addMetadata(path string, md metadata.Metadata, owner *user.User) error { if err := md.CheckValidity(); err != nil { - return errors.Wrap(ErrInvalidMetadata, err.Error()) + return errors.Wrap(err, "provided metadata is invalid") } data, err := proto.Marshal(md) @@ -285,47 +696,118 @@ func (m *Mount) addMetadata(path string, md metadata.Metadata) error { return err } - log.Printf("writing metadata to %q", path) - return m.writeDataAtomic(path, data) + mode := filePermissions + // If the file already exists, then preserve its owner and mode if + // possible. This is necessary because by default, for atomicity + // reasons we'll replace the file rather than overwrite it. + info, err := os.Lstat(path) + if err == nil { + if owner == nil && util.IsUserRoot() { + uid := info.Sys().(*syscall.Stat_t).Uid + if owner, err = util.UserFromUID(int64(uid)); err != nil { + log.Print(err) + } + } + mode = info.Mode() & 0777 + } else if !os.IsNotExist(err) { + log.Print(err) + } + + if owner != nil { + log.Printf("writing metadata to %q and setting owner to %s", path, owner.Username) + } else { + log.Printf("writing metadata to %q", path) + } + return m.writeData(path, data, owner, mode) +} + +// readMetadataFileSafe gets the contents of a metadata file extra-carefully, +// considering that it could be a malicious file created to cause a +// denial-of-service. Specifically, the following checks are done: +// +// - It must be a regular file, not another type of file like a symlink or FIFO. +// (Symlinks aren't bad by themselves, but given that a malicious user could +// point one to absolutely anywhere, and there is no known use case for the +// metadata files themselves being symlinks, it seems best to disallow them.) +// - It must have a reasonable size (<= maxMetadataFileSize). +// - If trustedUser is non-nil, then the file must be owned by the given user +// or by root. +// +// Take care to avoid TOCTOU (time-of-check-time-of-use) bugs when doing these +// tests. Notably, we must open the file before checking the file type, as the +// file type could change between any previous checks and the open. When doing +// this, O_NOFOLLOW is needed to avoid following a symlink (this applies to the +// last path component only), and O_NONBLOCK is needed to avoid blocking if the +// file is a FIFO. +// +// This function returns the data read as well as the UID of the user who owns +// the file. The returned UID is needed for login protectors, where the UID +// needs to be cross-checked with the UID stored in the file itself. +func readMetadataFileSafe(path string, trustedUser *user.User) ([]byte, int64, error) { + file, err := os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) + if err != nil { + return nil, -1, err + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + return nil, -1, err + } + if !info.Mode().IsRegular() { + return nil, -1, &ErrCorruptMetadata{path, errors.New("not a regular file")} + } + if !checkOwnership(path, info, trustedUser) { + return nil, -1, &ErrCorruptMetadata{path, errors.New("metadata file belongs to another user")} + } + // Clear O_NONBLOCK, since it has served its purpose when opening the + // file, and the behavior of reading from a regular file with O_NONBLOCK + // is technically unspecified. + if _, err = unix.FcntlInt(file.Fd(), unix.F_SETFL, 0); err != nil { + return nil, -1, &os.PathError{Op: "clearing O_NONBLOCK", Path: path, Err: err} + } + // Read the file contents, allowing at most maxMetadataFileSize bytes. + reader := &io.LimitedReader{R: file, N: maxMetadataFileSize + 1} + data, err := io.ReadAll(reader) + if err != nil { + return nil, -1, err + } + if reader.N == 0 { + return nil, -1, &ErrCorruptMetadata{path, errors.New("metadata file size limit exceeded")} + } + return data, int64(info.Sys().(*syscall.Stat_t).Uid), nil } // getMetadata reads the metadata structure from the file with the specified // path. Only reads normal metadata files, not linked metadata. -func (m *Mount) getMetadata(path string, md metadata.Metadata) error { - data, err := ioutil.ReadFile(path) +func (m *Mount) getMetadata(path string, trustedUser *user.User, md metadata.Metadata) (int64, error) { + data, owner, err := readMetadataFileSafe(path, trustedUser) if err != nil { - log.Printf("could not read metadata at %q", path) - if os.IsNotExist(err) { - return errors.Wrapf(ErrNoMetadata, "descriptor %s", filepath.Base(path)) - } - return err + log.Printf("could not read metadata from %q: %v", path, err) + return -1, err } if err := proto.Unmarshal(data, md); err != nil { - return errors.Wrap(ErrCorruptMetadata, err.Error()) + return -1, &ErrCorruptMetadata{path, err} } if err := md.CheckValidity(); err != nil { - log.Printf("metadata at %q is not valid", path) - return errors.Wrap(ErrCorruptMetadata, err.Error()) + return -1, &ErrCorruptMetadata{path, err} } log.Printf("successfully read metadata from %q", path) - return nil + return owner, nil } // removeMetadata deletes the metadata struct from the file with the specified // path. Works with regular or linked metadata. func (m *Mount) removeMetadata(path string) error { if err := os.Remove(path); err != nil { - log.Printf("could not remove metadata at %q", path) - if os.IsNotExist(err) { - return errors.Wrapf(ErrNoMetadata, "descriptor %s", filepath.Base(path)) - } + log.Printf("could not remove metadata file at %q: %v", path, err) return err } - log.Printf("successfully removed metadata at %q", path) + log.Printf("successfully removed metadata file at %q", path) return nil } @@ -333,148 +815,218 @@ func (m *Mount) removeMetadata(path string) error { // will overwrite the value of an existing protector with this descriptor. This // will fail with ErrLinkedProtector if a linked protector with this descriptor // already exists on the filesystem. -func (m *Mount) AddProtector(data *metadata.ProtectorData) error { - if err := m.CheckSetup(); err != nil { +func (m *Mount) AddProtector(data *metadata.ProtectorData, owner *user.User) error { + var err error + if err = m.CheckSetup(nil); err != nil { return err } if isRegularFile(m.linkedProtectorPath(data.ProtectorDescriptor)) { - return m.err(ErrLinkedProtector) + return errors.Errorf("cannot modify linked protector %s on filesystem %s", + data.ProtectorDescriptor, m.Path) } path := m.protectorPath(data.ProtectorDescriptor) - return m.err(m.addMetadata(path, data)) + return m.addMetadata(path, data, owner) } // AddLinkedProtector adds a link in this filesystem to the protector metadata -// in the dest filesystem. -func (m *Mount) AddLinkedProtector(descriptor string, dest *Mount) error { - if err := m.CheckSetup(); err != nil { - return err +// in the dest filesystem, if one doesn't already exist. On success, the return +// value is a nil error and a bool that is true iff the link is newly created. +func (m *Mount) AddLinkedProtector(descriptor string, dest *Mount, trustedUser *user.User, + ownerIfCreating *user.User) (bool, error) { + if err := m.CheckSetup(trustedUser); err != nil { + return false, err } // Check that the link is good (descriptor exists, filesystem has UUID). - if _, err := dest.GetRegularProtector(descriptor); err != nil { - return err + if _, err := dest.GetRegularProtector(descriptor, trustedUser); err != nil { + return false, err } - // Right now, we only make links using UUIDs. - link, err := makeLink(dest, "UUID") - if err != nil { - return dest.err(err) + linkPath := m.linkedProtectorPath(descriptor) + + // Check whether the link already exists. + existingLink, _, err := readMetadataFileSafe(linkPath, trustedUser) + if err == nil { + existingLinkedMnt, err := getMountFromLink(string(existingLink)) + if err != nil { + return false, errors.Wrap(err, linkPath) + } + if existingLinkedMnt != dest { + return false, errors.Errorf("link %q points to %q, but expected %q", + linkPath, existingLinkedMnt.Path, dest.Path) + } + return false, nil + } + if !os.IsNotExist(err) { + return false, err } - path := m.linkedProtectorPath(descriptor) - return m.err(m.writeDataAtomic(path, []byte(link))) + var newLink string + newLink, err = makeLink(dest) + if err != nil { + return false, err + } + return true, m.writeData(linkPath, []byte(newLink), ownerIfCreating, filePermissions) } // GetRegularProtector looks up the protector metadata by descriptor. This will -// fail with ErrNoMetadata if the descriptor is a linked protector. -func (m *Mount) GetRegularProtector(descriptor string) (*metadata.ProtectorData, error) { - if err := m.CheckSetup(); err != nil { +// fail with ErrProtectorNotFound if the descriptor is a linked protector. +func (m *Mount) GetRegularProtector(descriptor string, trustedUser *user.User) (*metadata.ProtectorData, error) { + if err := m.CheckSetup(trustedUser); err != nil { return nil, err } data := new(metadata.ProtectorData) path := m.protectorPath(descriptor) - return data, m.err(m.getMetadata(path, data)) + owner, err := m.getMetadata(path, trustedUser, data) + if os.IsNotExist(err) { + err = &ErrProtectorNotFound{descriptor, m} + } + if err != nil { + return nil, err + } + // Login protectors have their UID stored in the file. Since normally + // any user can create files in the fscrypt metadata directories, for a + // login protector to be considered valid it *must* be owned by the + // claimed user or by root. Note: fscrypt v0.3.2 and later always makes + // login protectors owned by the user, but previous versions could + // create them owned by root -- that is the main reason we allow root. + if data.Source == metadata.SourceType_pam_passphrase && owner != 0 && owner != data.Uid { + log.Printf("WARNING: %q claims to be the login protector for uid %d, but it is owned by uid %d. Needs to be %d or 0.", + path, data.Uid, owner, data.Uid) + return nil, &ErrCorruptMetadata{path, errors.New("login protector belongs to wrong user")} + } + return data, nil } // GetProtector returns the Mount of the filesystem containing the information // and that protector's data. If the descriptor is a regular (not linked) // protector, the mount will return itself. -func (m *Mount) GetProtector(descriptor string) (*Mount, *metadata.ProtectorData, error) { - if err := m.CheckSetup(); err != nil { +func (m *Mount) GetProtector(descriptor string, trustedUser *user.User) (*Mount, *metadata.ProtectorData, error) { + if err := m.CheckSetup(trustedUser); err != nil { return nil, nil, err } // Get the link data from the link file - link, err := ioutil.ReadFile(m.linkedProtectorPath(descriptor)) + path := m.linkedProtectorPath(descriptor) + link, _, err := readMetadataFileSafe(path, trustedUser) if err != nil { // If the link doesn't exist, try for a regular protector. if os.IsNotExist(err) { - data, err := m.GetRegularProtector(descriptor) + data, err := m.GetRegularProtector(descriptor, trustedUser) return m, data, err } - return nil, nil, m.err(err) + return nil, nil, err } - - // As the link could refer to multiple filesystems, we check each one - // for valid metadata. - mnts, err := getMountsFromLink(string(link)) + log.Printf("following protector link %s", path) + linkedMnt, err := getMountFromLink(string(link)) if err != nil { - return nil, nil, m.err(err) + return nil, nil, errors.Wrap(err, path) } - - for _, mnt := range mnts { - if data, err := mnt.GetRegularProtector(descriptor); err != nil { - log.Print(err) - } else { - return mnt, data, nil - } + data, err := linkedMnt.GetRegularProtector(descriptor, trustedUser) + if err != nil { + return nil, nil, &ErrFollowLink{string(link), err} } - return nil, nil, m.err(errors.Wrapf(ErrLinkExpired, "protector %s", descriptor)) + return linkedMnt, data, nil } -// RemoveProtector deletes the protector metadata (or an link to another +// RemoveProtector deletes the protector metadata (or a link to another // filesystem's metadata) from the filesystem storage. func (m *Mount) RemoveProtector(descriptor string) error { - if err := m.CheckSetup(); err != nil { + if err := m.CheckSetup(nil); err != nil { return err } // We first try to remove the linkedProtector. If that metadata does not // exist, we try to remove the normal protector. err := m.removeMetadata(m.linkedProtectorPath(descriptor)) - if errors.Cause(err) == ErrNoMetadata { + if os.IsNotExist(err) { err = m.removeMetadata(m.protectorPath(descriptor)) + if os.IsNotExist(err) { + err = &ErrProtectorNotFound{descriptor, m} + } } - return m.err(err) + return err } // ListProtectors lists the descriptors of all protectors on this filesystem. -// This does not include linked protectors. -func (m *Mount) ListProtectors() ([]string, error) { - if err := m.CheckSetup(); err != nil { - return nil, err - } - protectors, err := m.listDirectory(m.ProtectorDir()) - return protectors, m.err(err) +// This does not include linked protectors. If trustedUser is non-nil, then +// the protectors are restricted to those owned by the given user or by root. +func (m *Mount) ListProtectors(trustedUser *user.User) ([]string, error) { + return m.listMetadata(m.ProtectorDir(), "protectors", trustedUser) } // AddPolicy adds the policy metadata to the filesystem storage. -func (m *Mount) AddPolicy(data *metadata.PolicyData) error { - if err := m.CheckSetup(); err != nil { +func (m *Mount) AddPolicy(data *metadata.PolicyData, owner *user.User) error { + if err := m.CheckSetup(nil); err != nil { return err } - return m.err(m.addMetadata(m.policyPath(data.KeyDescriptor), data)) + return m.addMetadata(m.PolicyPath(data.KeyDescriptor), data, owner) } // GetPolicy looks up the policy metadata by descriptor. -func (m *Mount) GetPolicy(descriptor string) (*metadata.PolicyData, error) { - if err := m.CheckSetup(); err != nil { +func (m *Mount) GetPolicy(descriptor string, trustedUser *user.User) (*metadata.PolicyData, error) { + if err := m.CheckSetup(trustedUser); err != nil { return nil, err } data := new(metadata.PolicyData) - return data, m.err(m.getMetadata(m.policyPath(descriptor), data)) + _, err := m.getMetadata(m.PolicyPath(descriptor), trustedUser, data) + if os.IsNotExist(err) { + err = &ErrPolicyNotFound{descriptor, m} + } + return data, err } // RemovePolicy deletes the policy metadata from the filesystem storage. func (m *Mount) RemovePolicy(descriptor string) error { - if err := m.CheckSetup(); err != nil { + if err := m.CheckSetup(nil); err != nil { return err } - return m.err(m.removeMetadata(m.policyPath(descriptor))) + err := m.removeMetadata(m.PolicyPath(descriptor)) + if os.IsNotExist(err) { + err = &ErrPolicyNotFound{descriptor, m} + } + return err } -// ListPolicies lists the descriptors of all policies on this filesystem. -func (m *Mount) ListPolicies() ([]string, error) { - if err := m.CheckSetup(); err != nil { - return nil, err +// ListPolicies lists the descriptors of all policies on this filesystem. If +// trustedUser is non-nil, then the policies are restricted to those owned by +// the given user or by root. +func (m *Mount) ListPolicies(trustedUser *user.User) ([]string, error) { + return m.listMetadata(m.PolicyDir(), "policies", trustedUser) +} + +type namesAndTimes struct { + names []string + times []time.Time +} + +func (c namesAndTimes) Len() int { + return len(c.names) +} + +func (c namesAndTimes) Less(i, j int) bool { + return c.times[i].Before(c.times[j]) +} + +func (c namesAndTimes) Swap(i, j int) { + c.names[i], c.names[j] = c.names[j], c.names[i] + c.times[i], c.times[j] = c.times[j], c.times[i] +} + +func sortFileListByLastMtime(directoryPath string, names []string) error { + c := namesAndTimes{names: names, times: make([]time.Time, len(names))} + for i, name := range names { + fi, err := os.Lstat(filepath.Join(directoryPath, name)) + if err != nil { + return err + } + c.times[i] = fi.ModTime() } - policies, err := m.listDirectory(m.PolicyDir()) - return policies, m.err(err) + sort.Sort(c) + return nil } // listDirectory returns a list of descriptors for a metadata directory, // including files which are links to other filesystem's metadata. func (m *Mount) listDirectory(directoryPath string) ([]string, error) { - log.Printf("listing descriptors in %q", directoryPath) dir, err := os.Open(directoryPath) if err != nil { return nil, err @@ -486,12 +1038,52 @@ func (m *Mount) listDirectory(directoryPath string) ([]string, error) { return nil, err } - var descriptors []string + if SortDescriptorsByLastMtime { + if err := sortFileListByLastMtime(directoryPath, names); err != nil { + return nil, err + } + } + + descriptors := make([]string, 0, len(names)) for _, name := range names { // Be sure to include links as well descriptors = append(descriptors, strings.TrimSuffix(name, linkFileExtension)) } - - log.Printf("found %d descriptor(s)", len(descriptors)) return descriptors, nil } + +func (m *Mount) listMetadata(dirPath string, metadataType string, owner *user.User) ([]string, error) { + log.Printf("listing %s in %q", metadataType, dirPath) + if err := m.CheckSetup(owner); err != nil { + return nil, err + } + names, err := m.listDirectory(dirPath) + if err != nil { + return nil, err + } + filesIgnoredDescription := "" + if owner != nil { + filteredNames := make([]string, 0, len(names)) + uid := uint32(util.AtoiOrPanic(owner.Uid)) + for _, name := range names { + info, err := os.Lstat(filepath.Join(dirPath, name)) + if err != nil { + continue + } + fileUID := info.Sys().(*syscall.Stat_t).Uid + if fileUID != uid && fileUID != 0 { + continue + } + filteredNames = append(filteredNames, name) + } + numIgnored := len(names) - len(filteredNames) + if numIgnored != 0 { + filesIgnoredDescription = + fmt.Sprintf(" (ignored %d %s not owned by %s or root)", + numIgnored, metadataType, owner.Username) + } + names = filteredNames + } + log.Printf("found %d %s%s", len(names), metadataType, filesIgnoredDescription) + return names, nil +} diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index 04d5123..f9c34ae 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -21,11 +21,13 @@ package filesystem import ( "os" + "os/user" "path/filepath" - "reflect" + "syscall" "testing" - "github.com/pkg/errors" + "golang.org/x/sys/unix" + "google.golang.org/protobuf/proto" "github.com/google/fscrypt/crypto" "github.com/google/fscrypt/metadata" @@ -57,6 +59,19 @@ func getFakeProtector() *metadata.ProtectorData { } } +func getFakeLoginProtector(uid int64) *metadata.ProtectorData { + protector := getFakeProtector() + protector.Source = metadata.SourceType_pam_passphrase + protector.Uid = uid + protector.Costs = &metadata.HashingCosts{ + Time: 1, + Memory: 1 << 8, + Parallelism: 1, + } + protector.Salt = make([]byte, 16) + return protector +} + func getFakePolicy() *metadata.PolicyData { return &metadata.PolicyData{ KeyDescriptor: "0123456789abcdef", @@ -76,7 +91,7 @@ func getSetupMount(t *testing.T) (*Mount, error) { if err != nil { return nil, err } - return mnt, mnt.Setup() + return mnt, mnt.Setup(WorldWritable) } // Tests that the setup works and creates the correct files @@ -86,7 +101,7 @@ func TestSetup(t *testing.T) { t.Fatal(err) } - if err := mnt.CheckSetup(); err != nil { + if err := mnt.CheckSetup(nil); err != nil { t.Error(err) } @@ -109,6 +124,125 @@ func TestRemoveAllMetadata(t *testing.T) { } } +// isSymlink returns true if the path exists and is that of a symlink. +func isSymlink(path string) bool { + info, err := loggedLstat(path) + return err == nil && info.Mode()&os.ModeSymlink != 0 +} + +// Test that when MOUNTPOINT/.fscrypt is a pre-created symlink, fscrypt will +// create/delete the metadata at the location pointed to by the symlink. +// +// This is a helper function that is called twice: once to test an absolute +// symlink and once to test a relative symlink. +func testSetupWithSymlink(t *testing.T, mnt *Mount, symlinkTarget string, realDir string) { + rawBaseDir := filepath.Join(mnt.Path, baseDirName) + if err := os.Symlink(symlinkTarget, rawBaseDir); err != nil { + t.Fatal(err) + } + defer os.Remove(rawBaseDir) + + if err := mnt.Setup(WorldWritable); err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + if err := mnt.CheckSetup(nil); err != nil { + t.Fatal(err) + } + if !isSymlink(rawBaseDir) { + t.Fatal("base dir should still be a symlink") + } + if !isDir(realDir) { + t.Fatal("real base dir should exist") + } + if err := mnt.RemoveAllMetadata(); err != nil { + t.Fatal(err) + } + if !isSymlink(rawBaseDir) { + t.Fatal("base dir should still be a symlink") + } + if isDir(realDir) { + t.Fatal("real base dir should no longer exist") + } +} + +func TestSetupWithAbsoluteSymlink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Fatal(err) + } + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + realDir := filepath.Join(tempDir, "realDir") + if realDir, err = filepath.Abs(realDir); err != nil { + t.Fatal(err) + } + testSetupWithSymlink(t, mnt, realDir, realDir) +} + +func TestSetupWithRelativeSymlink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Fatal(err) + } + realDir := filepath.Join(mnt.Path, ".fscrypt-real") + testSetupWithSymlink(t, mnt, ".fscrypt-real", realDir) +} + +func testSetupMode(t *testing.T, mnt *Mount, setupMode SetupMode, expectedPerms os.FileMode) { + mnt.RemoveAllMetadata() + if err := mnt.Setup(setupMode); err != nil { + t.Fatal(err) + } + dirNames := []string{"policies", "protectors"} + for _, dirName := range dirNames { + fi, err := os.Stat(filepath.Join(mnt.Path, ".fscrypt", dirName)) + if err != nil { + t.Fatal(err) + } + if fi.Mode()&(os.ModeSticky|0777) != expectedPerms { + t.Errorf("directory %s doesn't have permissions %o", dirName, expectedPerms) + } + } +} + +// Tests that the supported setup modes (WorldWritable and SingleUserWritable) +// work as intended. +func TestSetupModes(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + testSetupMode(t, mnt, WorldWritable, os.ModeSticky|0777) + testSetupMode(t, mnt, SingleUserWritable, 0755) +} + +// Tests that fscrypt refuses to use metadata directories that are +// world-writable but don't have the sticky bit set. +func TestInsecurePermissions(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + if err = mnt.Setup(WorldWritable); err != nil { + t.Fatal(err) + } + if err = os.Chmod(mnt.PolicyDir(), 0777); err != nil { + t.Fatal(err) + } + defer os.Chmod(mnt.PolicyDir(), os.ModeSticky|0777) + err = mnt.CheckSetup(nil) + if _, ok := err.(*ErrInsecurePermissions); !ok { + t.Fatal("expected ErrInsecurePermissions") + } +} + // Adding a good Protector should succeed, adding a bad one should fail func TestAddProtector(t *testing.T) { mnt, err := getSetupMount(t) @@ -118,31 +252,31 @@ func TestAddProtector(t *testing.T) { defer mnt.RemoveAllMetadata() protector := getFakeProtector() - if err = mnt.AddProtector(protector); err != nil { + if err = mnt.AddProtector(protector, nil); err != nil { t.Error(err) } // Change the source to bad one, or one that requires hashing costs protector.Source = metadata.SourceType_default - if mnt.AddProtector(protector) == nil { + if mnt.AddProtector(protector, nil) == nil { t.Error("bad source for a descriptor should make metadata invalid") } protector.Source = metadata.SourceType_custom_passphrase - if mnt.AddProtector(protector) == nil { + if mnt.AddProtector(protector, nil) == nil { t.Error("protectors using passphrases should require hashing costs") } protector.Source = metadata.SourceType_raw_key // Use a bad wrapped key protector.WrappedKey = wrappedPolicyKey - if mnt.AddProtector(protector) == nil { + if mnt.AddProtector(protector, nil) == nil { t.Error("bad length for protector keys should make metadata invalid") } protector.WrappedKey = wrappedProtectorKey // Change the descriptor (to a bad length) protector.ProtectorDescriptor = "abcde" - if mnt.AddProtector(protector) == nil { + if mnt.AddProtector(protector, nil) == nil { t.Error("bad descriptor length should make metadata invalid") } @@ -157,32 +291,32 @@ func TestAddPolicy(t *testing.T) { defer mnt.RemoveAllMetadata() policy := getFakePolicy() - if err = mnt.AddPolicy(policy); err != nil { + if err = mnt.AddPolicy(policy, nil); err != nil { t.Error(err) } // Bad encryption options should make policy invalid policy.Options.Padding = 7 - if mnt.AddPolicy(policy) == nil { + if mnt.AddPolicy(policy, nil) == nil { t.Error("padding not a power of 2 should make metadata invalid") } policy.Options.Padding = 16 policy.Options.Filenames = metadata.EncryptionOptions_default - if mnt.AddPolicy(policy) == nil { + if mnt.AddPolicy(policy, nil) == nil { t.Error("encryption mode not set should make metadata invalid") } policy.Options.Filenames = metadata.EncryptionOptions_AES_256_CTS // Use a bad wrapped key policy.WrappedPolicyKeys[0].WrappedKey = wrappedProtectorKey - if mnt.AddPolicy(policy) == nil { + if mnt.AddPolicy(policy, nil) == nil { t.Error("bad length for policy keys should make metadata invalid") } policy.WrappedPolicyKeys[0].WrappedKey = wrappedPolicyKey // Change the descriptor (to a bad length) policy.KeyDescriptor = "abcde" - if mnt.AddPolicy(policy) == nil { + if mnt.AddPolicy(policy, nil) == nil { t.Error("bad descriptor length should make metadata invalid") } } @@ -196,16 +330,16 @@ func TestSetPolicy(t *testing.T) { defer mnt.RemoveAllMetadata() policy := getFakePolicy() - if err = mnt.AddPolicy(policy); err != nil { + if err = mnt.AddPolicy(policy, nil); err != nil { t.Fatal(err) } - realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor) + realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor, nil) if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(realPolicy, policy) { + if !proto.Equal(realPolicy, policy) { t.Errorf("policy %+v does not equal expected policy %+v", realPolicy, policy) } @@ -220,20 +354,99 @@ func TestSetProtector(t *testing.T) { defer mnt.RemoveAllMetadata() protector := getFakeProtector() - if err = mnt.AddProtector(protector); err != nil { + if err = mnt.AddProtector(protector, nil); err != nil { t.Fatal(err) } - realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor) + realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(realProtector, protector) { + if !proto.Equal(realProtector, protector) { t.Errorf("protector %+v does not equal expected protector %+v", realProtector, protector) } } +// Tests that a login protector whose embedded UID doesn't match the file owner +// is considered invalid. (Such a file could be created by a malicious user to +// try to confuse fscrypt into processing the wrong file.) +func TestSpoofedLoginProtector(t *testing.T) { + myUID := int64(os.Geteuid()) + badUID := myUID + 1 // anything different from myUID + mnt, err := getSetupMount(t) + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + // Control case: protector with matching UID should be accepted. + protector := getFakeLoginProtector(myUID) + if err = mnt.AddProtector(protector, nil); err != nil { + t.Fatal(err) + } + _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) + if err != nil { + t.Fatal(err) + } + if err = mnt.RemoveProtector(protector.ProtectorDescriptor); err != nil { + t.Fatal(err) + } + + // The real test: protector with mismatching UID should rejected, + // *unless* the process running the tests (and hence the file owner) is + // root in which case it should be accepted. + protector = getFakeLoginProtector(badUID) + if err = mnt.AddProtector(protector, nil); err != nil { + t.Fatal(err) + } + _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) + if myUID == 0 { + if err != nil { + t.Fatal(err) + } + } else { + if err == nil { + t.Fatal("reading protector with bad UID unexpectedly succeeded") + } + } +} + +// Tests that the fscrypt metadata files are given mode 0600. +func TestMetadataFileMode(t *testing.T) { + mnt, err := getSetupMount(t) + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + // Policy + policy := getFakePolicy() + if err = mnt.AddPolicy(policy, nil); err != nil { + t.Fatal(err) + } + fi, err := os.Stat(filepath.Join(mnt.Path, ".fscrypt/policies/", policy.KeyDescriptor)) + if err != nil { + t.Fatal(err) + } + if fi.Mode()&0777 != 0600 { + t.Error("Policy file has wrong mode") + } + + // Protector + protector := getFakeProtector() + if err = mnt.AddProtector(protector, nil); err != nil { + t.Fatal(err) + } + fi, err = os.Stat(filepath.Join(mnt.Path, ".fscrypt/protectors", protector.ProtectorDescriptor)) + if err != nil { + t.Fatal(err) + } + if fi.Mode()&0777 != 0600 { + t.Error("Protector file has wrong mode") + } +} + // Gets a setup mount and a fake second mount func getTwoSetupMounts(t *testing.T) (realMnt, fakeMnt *Mount, err error) { if realMnt, err = getSetupMount(t); err != nil { @@ -245,8 +458,8 @@ func getTwoSetupMounts(t *testing.T) (realMnt, fakeMnt *Mount, err error) { if err = os.MkdirAll(fakeMountpoint, basePermissions); err != nil { return } - fakeMnt = &Mount{Path: fakeMountpoint} - err = fakeMnt.Setup() + fakeMnt = &Mount{Path: fakeMountpoint, FilesystemType: realMnt.FilesystemType} + err = fakeMnt.Setup(WorldWritable) return } @@ -266,22 +479,32 @@ func TestLinkedProtector(t *testing.T) { // Add the protector to the first filesystem protector := getFakeProtector() - if err = realMnt.AddProtector(protector); err != nil { + if err = realMnt.AddProtector(protector, nil); err != nil { t.Fatal(err) } // Add the link to the second filesystem - if err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt); err != nil { + var isNewLink bool + if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil { t.Fatal(err) } + if !isNewLink { + t.Fatal("Link was not new") + } + if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil { + t.Fatal(err) + } + if isNewLink { + t.Fatal("Link was new") + } // Get the protector though the second system - _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor) - if errors.Cause(err) != ErrNoMetadata { + _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor, nil) + if _, ok := err.(*ErrProtectorNotFound); !ok { t.Fatal(err) } - retMnt, retProtector, err := fakeMnt.GetProtector(protector.ProtectorDescriptor) + retMnt, retProtector, err := fakeMnt.GetProtector(protector.ProtectorDescriptor, nil) if err != nil { t.Fatal(err) } @@ -289,7 +512,99 @@ func TestLinkedProtector(t *testing.T) { t.Error("mount returned was incorrect") } - if !reflect.DeepEqual(retProtector, protector) { + if !proto.Equal(retProtector, protector) { t.Errorf("protector %+v does not equal expected protector %+v", retProtector, protector) } } + +func createFile(path string, size int64) error { + if err := os.WriteFile(path, []byte{}, 0600); err != nil { + return err + } + return os.Truncate(path, size) +} + +// Tests the readMetadataFileSafe() function. +func TestReadMetadataFileSafe(t *testing.T) { + currentUser, err := util.EffectiveUser() + otherUser := &user.User{Uid: "-1"} + if err != nil { + t.Fatal(err) + } + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + filePath := filepath.Join(tempDir, "file") + defer os.RemoveAll(tempDir) + + // Good file (control case) + if err = createFile(filePath, 1000); err != nil { + t.Fatal(err) + } + _, owner, err := readMetadataFileSafe(filePath, nil) + if err != nil { + t.Fatal("failed to read file") + } + if owner != int64(os.Geteuid()) { + t.Fatal("got wrong owner") + } + // Also try it with the trustedUser argument set to the current user. + if _, _, err = readMetadataFileSafe(filePath, currentUser); err != nil { + t.Fatal("failed to read file") + } + os.Remove(filePath) + + // File owned by another user. We might not have permission to actually + // change the file's ownership, so we simulate this by passing in a bad + // value for the trustedUser argument. + if err = createFile(filePath, 1000); err != nil { + t.Fatal(err) + } + _, _, err = readMetadataFileSafe(filePath, otherUser) + if util.IsUserRoot() { + if err != nil { + t.Fatal("root-owned file didn't pass owner validation") + } + } else { + if err == nil { + t.Fatal("unexpectedly could read file owned by another user") + } + } + os.Remove(filePath) + + // Nonexistent file + _, _, err = readMetadataFileSafe(filePath, nil) + if !os.IsNotExist(err) { + t.Fatal("trying to read nonexistent file didn't fail with expected error") + } + + // Symlink + if err = os.Symlink("target", filePath); err != nil { + t.Fatal(err) + } + _, _, err = readMetadataFileSafe(filePath, nil) + if err.(*os.PathError).Err != syscall.ELOOP { + t.Fatal("trying to read symlink didn't fail with ELOOP") + } + os.Remove(filePath) + + // FIFO + if err = unix.Mkfifo(filePath, 0600); err != nil { + t.Fatal(err) + } + _, _, err = readMetadataFileSafe(filePath, nil) + if _, ok := err.(*ErrCorruptMetadata); !ok { + t.Fatal("trying to read FIFO didn't fail with expected error") + } + os.Remove(filePath) + + // Very large file + if err = createFile(filePath, 1000000); err != nil { + t.Fatal(err) + } + _, _, err = readMetadataFileSafe(filePath, nil) + if _, ok := err.(*ErrCorruptMetadata); !ok { + t.Fatal("trying to read very large file didn't fail with expected error") + } +} diff --git a/filesystem/mountpoint.go b/filesystem/mountpoint.go index 12016dd..9be54a4 100644 --- a/filesystem/mountpoint.go +++ b/filesystem/mountpoint.go @@ -22,24 +22,14 @@ package filesystem import ( - "io/ioutil" - "os" -) - -/* -#include <mntent.h> // 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" - -import ( + "bufio" "fmt" + "io" "log" + "os" "path/filepath" "sort" + "strconv" "strings" "sync" @@ -47,86 +37,326 @@ import ( ) var ( - // These maps hold data about the state of the system's mountpoints. + // These maps hold data about the state of the system's filesystems. + // + // They only contain one Mount per filesystem, even if there are + // additional bind mounts, since we want to store fscrypt metadata in + // only one place per filesystem. When it is ambiguous which Mount + // should be used for a filesystem, mountsByDevice will contain an + // explicit nil entry, and mountsByPath won't contain an entry. + mountsByDevice map[DeviceNumber]*Mount 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" + pathToken = "PATH" // 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 +// Unescape octal-encoded escape sequences in a string from the mountinfo file. +// The kernel encodes the ' ', '\t', '\n', and '\\' bytes this way. This +// function exactly inverts what the kernel does, including by preserving +// invalid UTF-8. +func unescapeString(str string) string { + var sb strings.Builder + for i := 0; i < len(str); i++ { + b := str[i] + if b == '\\' && i+3 < len(str) { + if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil { + b = uint8(parsed) + i += 3 + } + } + sb.WriteByte(b) } + return sb.String() +} - // make new maps - mountsByPath = make(map[string]*Mount) - mountsByDevice = make(map[string][]*Mount) +// EscapeString is the reverse of unescapeString. Use this to avoid injecting +// spaces or newlines into output that uses these characters as separators. +func EscapeString(str string) string { + var sb strings.Builder + for _, b := range []byte(str) { + switch b { + case ' ', '\t', '\n', '\\': + sb.WriteString(fmt.Sprintf("\\%03o", b)) + default: + sb.WriteByte(b) + } + } + return sb.String() +} - // 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)) +func getDeviceName(num DeviceNumber, mountSource string) string { + // When possible, get the device name via the device number rather than + // use the mount source field directly. This is necessary to handle a + // rootfs that was mounted via the kernel command line, since mountinfo + // always shows /dev/root for that. + linkPath := fmt.Sprintf("/sys/dev/block/%v", num) + if target, err := os.Readlink(linkPath); err == nil { + derivedDeviceName := fmt.Sprintf("/dev/%s", filepath.Base(target)) + if _, err := os.Stat(derivedDeviceName); err == nil { + return derivedDeviceName + } + } + // Sysfs is not mounted or is incomplete, or the device nodes are not in + // the standard location. Fall back to using the mount source field if + // it looks like a path. + if strings.HasPrefix(mountSource, "/") { + return mountSource } - defer C.endmntent(fileHandle) + return "" +} - for { - entry := C.getmntent(fileHandle) - // When getmntent returns nil, we have read all of the entries. - if entry == nil { - mountsInitialized = true +// Parse one line of /proc/self/mountinfo. +// +// The line contains the following space-separated fields: +// +// [0] mount ID +// [1] parent ID +// [2] major:minor +// [3] root +// [4] mount point +// [5] mount options +// [6...n-1] optional field(s) +// [n] separator +// [n+1] filesystem type +// [n+2] mount source +// [n+3] super options +// +// For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt +func parseMountInfoLine(line string) *Mount { + fields := strings.Split(line, " ") + if len(fields) < 10 { + return nil + } + + // Count the optional fields. In case new fields are appended later, + // don't simply assume that n == len(fields) - 4. + n := 6 + for fields[n] != "-" { + n++ + if n >= len(fields) { return nil } + } + if n+3 >= len(fields) { + return nil + } + + var mnt *Mount = &Mount{} + var err error + mnt.DeviceNumber, err = newDeviceNumberFromString(fields[2]) + if err != nil { + return nil + } + mnt.Subtree = unescapeString(fields[3]) + mnt.Path = unescapeString(fields[4]) + for _, opt := range strings.Split(fields[5], ",") { + if opt == "ro" { + mnt.ReadOnly = true + } + } + mnt.FilesystemType = unescapeString(fields[n+1]) + mnt.Device = getDeviceName(mnt.DeviceNumber, unescapeString(fields[n+2])) + return mnt +} + +type mountpointTreeNode struct { + mount *Mount + parent *mountpointTreeNode + children []*mountpointTreeNode +} - // Create the Mount structure by converting types. - mnt := Mount{ - Path: C.GoString(entry.mnt_dir), - Filesystem: C.GoString(entry.mnt_type), - Options: strings.Split(C.GoString(entry.mnt_opts), ","), +func addUncontainedSubtreesRecursive(dst map[string]bool, + node *mountpointTreeNode, allUncontainedSubtrees map[string]bool) { + if allUncontainedSubtrees[node.mount.Subtree] { + dst[node.mount.Subtree] = true + } + for _, child := range node.children { + addUncontainedSubtreesRecursive(dst, child, allUncontainedSubtrees) + } +} + +// findMainMount finds the "main" Mount of a filesystem. The "main" Mount is +// where the filesystem's fscrypt metadata is stored. +// +// Normally, there is just one Mount and it's of the entire filesystem +// (mnt.Subtree == "/"). But in general, the filesystem might be mounted in +// multiple places, including "bind mounts" where mnt.Subtree != "/". Also, the +// filesystem might have a combination of read-write and read-only mounts. +// +// To handle most cases, we could just choose a mount with mnt.Subtree == "/", +// preferably a read-write mount. However, that doesn't work in containers +// where the "/" subtree might not be mounted. Here's a real-world example: +// +// mnt.Subtree mnt.Path +// ----------- -------- +// /var/lib/lxc/base/rootfs / +// /var/cache/pacman/pkg /var/cache/pacman/pkg +// /srv/repo/x86_64 /srv/http/x86_64 +// +// In this case, all mnt.Subtree are independent. To handle this case, we must +// choose the Mount whose mnt.Path contains the others, i.e. the first one. +// Note: the fscrypt metadata won't be usable from outside the container since +// it won't be at the real root of the filesystem, but that may be acceptable. +// +// However, we can't look *only* at mnt.Path, since in some cases mnt.Subtree is +// needed to correctly handle bind mounts. For example, in the following case, +// the first Mount should be chosen: +// +// mnt.Subtree mnt.Path +// ----------- -------- +// /foo /foo +// /foo/dir /dir +// +// To solve this, we divide the mounts into non-overlapping trees of mnt.Path. +// Then, we choose one of these trees which contains (exactly or via path +// prefix) *all* mnt.Subtree. We then return the root of this tree. In both +// the above examples, this algorithm returns the first Mount. +func findMainMount(filesystemMounts []*Mount) *Mount { + // Index this filesystem's mounts by path. Note: paths are unique here, + // since non-last mounts were already excluded earlier. + // + // Also build the set of all mounted subtrees. + filesystemMountsByPath := make(map[string]*mountpointTreeNode) + allSubtrees := make(map[string]bool) + for _, mnt := range filesystemMounts { + filesystemMountsByPath[mnt.Path] = &mountpointTreeNode{mount: mnt} + allSubtrees[mnt.Subtree] = true + } + + // Divide the mounts into non-overlapping trees of mountpoints. + for path, mntNode := range filesystemMountsByPath { + for path != "/" && mntNode.parent == nil { + path = filepath.Dir(path) + if parent := filesystemMountsByPath[path]; parent != nil { + mntNode.parent = parent + parent.children = append(parent.children, mntNode) + } } + } - // Skip invalid mountpoints - var err error - if mnt.Path, err = cannonicalizePath(mnt.Path); err != nil { - log.Printf("getting mnt_dir: %v", err) + // Build the set of mounted subtrees that aren't contained in any other + // mounted subtree. + allUncontainedSubtrees := make(map[string]bool) + for subtree := range allSubtrees { + contained := false + for t := subtree; t != "/" && !contained; { + t = filepath.Dir(t) + contained = allSubtrees[t] + } + if !contained { + allUncontainedSubtrees[subtree] = true + } + } + + // Select the root of a mountpoint tree whose mounted subtrees contain + // *all* mounted subtrees. Equivalently, select a mountpoint tree in + // which every uncontained subtree is mounted. + var mainMount *Mount + for _, mntNode := range filesystemMountsByPath { + mnt := mntNode.mount + if mntNode.parent != nil { + continue + } + uncontainedSubtrees := make(map[string]bool) + addUncontainedSubtreesRecursive(uncontainedSubtrees, mntNode, allUncontainedSubtrees) + if len(uncontainedSubtrees) != len(allUncontainedSubtrees) { + continue + } + // If there's more than one eligible mount, they should have the + // same Subtree. Otherwise it's ambiguous which one to use. + if mainMount != nil && mainMount.Subtree != mnt.Subtree { + log.Printf("Unsupported case: %q (%v) has multiple non-overlapping mounts. This filesystem will be ignored!", + mnt.Device, mnt.DeviceNumber) + return nil + } + // Prefer a read-write mount to a read-only one. + if mainMount == nil || mainMount.ReadOnly { + mainMount = mnt + } + } + return mainMount +} + +// This is separate from loadMountInfo() only for unit testing. +func readMountInfo(r io.Reader) error { + mountsByDevice = make(map[DeviceNumber]*Mount) + mountsByPath = make(map[string]*Mount) + allMountsByDevice := make(map[DeviceNumber][]*Mount) + allMountsByPath := make(map[string]*Mount) + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + mnt := parseMountInfoLine(line) + if mnt == nil { + log.Printf("ignoring invalid mountinfo line %q", line) 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) + log.Printf("ignoring mountpoint %q because it is 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 := cannonicalizePath(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) + // mountpoints are listed in mount order. + allMountsByPath[mnt.Path] = mnt + } + // For each filesystem, choose a "main" Mount and discard any additional + // bind mounts. fscrypt only cares about the main Mount, since it's + // where the fscrypt metadata is stored. Store all the main Mounts in + // mountsByDevice and mountsByPath so that they can be found later. + for _, mnt := range allMountsByPath { + allMountsByDevice[mnt.DeviceNumber] = + append(allMountsByDevice[mnt.DeviceNumber], mnt) + } + for deviceNumber, filesystemMounts := range allMountsByDevice { + mnt := findMainMount(filesystemMounts) + mountsByDevice[deviceNumber] = mnt // may store an explicit nil entry + if mnt != nil { + mountsByPath[mnt.Path] = mnt + } + } + return nil +} + +// loadMountInfo populates the Mount mappings by parsing /proc/self/mountinfo. +// It returns an error if the Mount mappings cannot be populated. +func loadMountInfo() error { + if !mountsInitialized { + file, err := os.Open("/proc/self/mountinfo") + if err != nil { + return err } + defer file.Close() + if err := readMountInfo(file); err != nil { + return err + } + mountsInitialized = true } + return nil } -// AllFilesystems lists all the Mounts on the current system ordered by path. -// Use CheckSetup() to see if they are used with fscrypt. +func filesystemLacksMainMountError(deviceNumber DeviceNumber) error { + return errors.Errorf("Device %q (%v) lacks a \"main\" mountpoint in the current mount namespace, so it's ambiguous where to store the fscrypt metadata.", + getDeviceName(deviceNumber, ""), deviceNumber) +} + +// AllFilesystems lists all mounted filesystems ordered by path to their "main" +// Mount. Use CheckSetup() to see if they are set up for use with fscrypt. func AllFilesystems() ([]*Mount, error) { mountMutex.Lock() defer mountMutex.Unlock() - if err := getMountInfo(); err != nil { + if err := loadMountInfo(); err != nil { return nil, err } @@ -145,135 +375,217 @@ func UpdateMountInfo() error { mountMutex.Lock() defer mountMutex.Unlock() mountsInitialized = false - return getMountInfo() + return loadMountInfo() } -// 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. +// FindMount returns the main Mount object for the filesystem which contains the +// file at the specified path. An error is returned if the path is invalid or if +// we cannot load the required mount data. If a mount 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 := cannonicalizePath(path) + mountMutex.Lock() + defer mountMutex.Unlock() + if err := loadMountInfo(); err != nil { + return nil, err + } + // First try to find the mount by the number of the containing device. + deviceNumber, err := getNumberOfContainingDevice(path) if err != nil { return nil, err } - - mountMutex.Lock() - defer mountMutex.Unlock() - if err = getMountInfo(); err != nil { + mnt, ok := mountsByDevice[deviceNumber] + if ok { + if mnt == nil { + return nil, filesystemLacksMainMountError(deviceNumber) + } + return mnt, nil + } + // The mount couldn't be found by the number of the containing device. + // Fall back to walking up the directory hierarchy and checking for a + // mount at each directory path. This is necessary for btrfs, where + // files report a different st_dev from the /proc/self/mountinfo entry. + curPath, err := canonicalizePath(path) + if err != nil { return nil, err } - - // Traverse up the directory tree until we find a mountpoint for { - if mnt, ok := mountsByPath[path]; ok { + mnt := mountsByPath[curPath] + if mnt != nil { 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) + parent := filepath.Dir(curPath) + if parent == curPath { + return nil, errors.Errorf("couldn't find mountpoint containing %q", path) } - path = parent + curPath = 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. +// GetMount is like FindMount, except GetMount also returns an error if the path +// doesn't name the same file as the filesystem's "main" Mount. For example, if +// a filesystem is fully mounted at "/mnt" and if "/mnt/a" exists, then +// FindMount("/mnt/a") will succeed whereas GetMount("/mnt/a") will fail. This +// is true even if "/mnt/a" is a bind mount of part of the same filesystem. func GetMount(mountpoint string) (*Mount, error) { - mountpoint, err := cannonicalizePath(mountpoint) + mnt, err := FindMount(mountpoint) + if err != nil { + return nil, &ErrNotAMountpoint{mountpoint} + } + // Check whether 'mountpoint' names the same directory as 'mnt.Path'. + // Use os.SameFile() (i.e., compare inode numbers) rather than compare + // canonical paths, since filesystems may be mounted in multiple places. + fi1, err := os.Stat(mountpoint) if err != nil { return nil, err } - - mountMutex.Lock() - defer mountMutex.Unlock() - if err = getMountInfo(); err != nil { + fi2, err := os.Stat(mnt.Path) + if err != nil { return nil, err } - - if mnt, ok := mountsByPath[mountpoint]; ok { - return mnt, nil + if !os.SameFile(fi1, fi2) { + return nil, &ErrNotAMountpoint{mountpoint} } - - return nil, errors.Wrap(ErrNotAMountpoint, mountpoint) + return mnt, nil } -// getMountsFromLink returns the Mount objects which match the provided link. -// This link if formatted as a tag (e.g. <token>=<value>) similar to how they -// apprear in "/etc/fstab". Currently, only "UUID" tokens are supported. Note -// that this can match multiple Mounts (due to the existance 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 in 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 := cannonicalizePath(searchPath) - if err != nil { - return nil, errors.Wrapf(ErrFollowLink, "no device with UUID %q", value) - } +func uuidToDeviceNumber(uuid string) (DeviceNumber, error) { + uuidSymlinkPath := filepath.Join(uuidDirectory, uuid) + return getDeviceNumber(uuidSymlinkPath) +} - // Lookup mountpoints for device in global store +func deviceNumberToMount(deviceNumber DeviceNumber) (*Mount, bool) { 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) + if err := loadMountInfo(); err != nil { + log.Print(err) + return nil, false } - return mnts, nil + mnt, ok := mountsByDevice[deviceNumber] + return mnt, ok } -// makeLink returns a link of the form <token>=<value> 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) +// getMountFromLink returns the main Mount, if any, for the filesystem which the +// given link points to. The link should contain a series of token-value pairs +// (<token>=<value>), one per line. The supported tokens are "UUID" and "PATH". +// If the UUID is present and it works, then it is used; otherwise, PATH is used +// if it is present. (The fallback from UUID to PATH will keep the link working +// if the UUID of the target filesystem changes but its mountpoint doesn't.) +// +// If a mount has been updated since the last call to one of the mount +// functions, make sure to run UpdateMountInfo first. +func getMountFromLink(link string) (*Mount, error) { + // Parse the link. + uuid := "" + path := "" + lines := strings.Split(link, "\n") + for _, line := range lines { + line := strings.TrimSpace(line) + if line == "" { + continue + } + pair := strings.Split(line, "=") + if len(pair) != 2 { + log.Printf("ignoring invalid line in filesystem link file: %q", line) + continue + } + token := pair[0] + value := pair[1] + switch token { + case uuidToken: + uuid = value + case pathToken: + path = value + default: + log.Printf("ignoring unknown link token %q", token) + } + } + // At least one of UUID and PATH must be present. + if uuid == "" && path == "" { + return nil, &ErrFollowLink{link, errors.Errorf("invalid filesystem link file")} + } + + // Try following the UUID. + errMsg := "" + if uuid != "" { + deviceNumber, err := uuidToDeviceNumber(uuid) + if err == nil { + mnt, ok := deviceNumberToMount(deviceNumber) + if mnt != nil { + log.Printf("resolved filesystem link using UUID %q", uuid) + return mnt, nil + } + if ok { + return nil, &ErrFollowLink{link, filesystemLacksMainMountError(deviceNumber)} + } + log.Printf("cannot find filesystem with UUID %q", uuid) + } else { + log.Printf("cannot find filesystem with UUID %q: %v", uuid, err) + } + errMsg += fmt.Sprintf("cannot find filesystem with UUID %q", uuid) + if path != "" { + log.Printf("falling back to using mountpoint path instead of UUID") + } } - if mnt.Device == "" { - return "", errors.Wrapf(ErrMakeLink, "no device for mount %q", mnt.Path) + // UUID didn't work. As a fallback, try the mountpoint path. + if path != "" { + mnt, err := GetMount(path) + if mnt != nil { + log.Printf("resolved filesystem link using mountpoint path %q", path) + return mnt, nil + } + log.Print(err) + if errMsg == "" { + errMsg = fmt.Sprintf("cannot find filesystem with main mountpoint %q", path) + } else { + errMsg += fmt.Sprintf(" or main mountpoint %q", path) + } } + // No method worked; return an error. + return nil, &ErrFollowLink{link, errors.New(errMsg)} +} - dirContents, err := ioutil.ReadDir(uuidDirectory) +func (mnt *Mount) getFilesystemUUID() (string, error) { + dirEntries, err := os.ReadDir(uuidDirectory) if err != nil { - return "", errors.Wrap(ErrMakeLink, err.Error()) + return "", err } - for _, fileInfo := range dirContents { + for _, dirEntry := range dirEntries { + fileInfo, err := dirEntry.Info() + if err != nil { + continue + } if fileInfo.Mode()&os.ModeSymlink == 0 { continue // Only interested in UUID symlinks } uuid := fileInfo.Name() - devicePath, err := cannonicalizePath(filepath.Join(uuidDirectory, uuid)) + deviceNumber, err := uuidToDeviceNumber(uuid) if err != nil { log.Print(err) continue } - if mnt.Device == devicePath { - return fmt.Sprintf("%s=%s", uuidToken, uuid), nil + if mnt.DeviceNumber == deviceNumber { + return uuid, nil } } - return "", errors.Wrapf(ErrMakeLink, "device %q has no UUID", mnt.Device) + return "", errors.Errorf("cannot determine UUID of device %q (%v)", + mnt.Device, mnt.DeviceNumber) +} + +// makeLink creates the contents of a link file which will point to the given +// filesystem. This will normally be a string of the form +// "UUID=<uuid>\nPATH=<path>\n". If the UUID cannot be determined, the UUID +// portion will be omitted. +func makeLink(mnt *Mount) (string, error) { + uuid, err := mnt.getFilesystemUUID() + if err != nil { + // The UUID could not be determined. This happens for btrfs + // filesystems, as the device number found via + // /dev/disk/by-uuid/* for btrfs filesystems differs from the + // actual device number of the mounted filesystem. Just rely + // entirely on the fallback to mountpoint path. + log.Print(err) + return fmt.Sprintf("%s=%s\n", pathToken, mnt.Path), nil + } + return fmt.Sprintf("%s=%s\n%s=%s\n", uuidToken, uuid, pathToken, mnt.Path), nil } diff --git a/filesystem/mountpoint_test.go b/filesystem/mountpoint_test.go index 73904a2..fd7e05d 100644 --- a/filesystem/mountpoint_test.go +++ b/filesystem/mountpoint_test.go @@ -17,9 +17,18 @@ * the License. */ +// Note: these tests assume the existence of some well-known directories: /mnt, +// /home, and /tmp. This is because the mountpoint loading code only retains +// mountpoints on valid directories. + package filesystem import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" "testing" ) @@ -29,6 +38,504 @@ func TestLoadMountInfo(t *testing.T) { } } +// Lock the mount maps so that concurrent tests don't interfere with each other. +func beginLoadMountInfoTest() { + mountMutex.Lock() +} + +func endLoadMountInfoTest() { + // Invalidate the fake mount information in case a test runs later which + // needs the real mount information. + mountsInitialized = false + mountMutex.Unlock() +} + +func loadMountInfoFromString(str string) { + readMountInfo(strings.NewReader(str)) +} + +func mountForDevice(deviceNumberStr string) *Mount { + deviceNumber, _ := newDeviceNumberFromString(deviceNumberStr) + return mountsByDevice[deviceNumber] +} + +// Test basic loading of a single mountpoint. +func TestLoadMountInfoBasic(t *testing.T) { + var mountinfo = ` +15 0 259:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + if len(mountsByDevice) != 1 { + t.Error("Loaded wrong number of mounts") + } + mnt := mountForDevice("259:3") + if mnt == nil { + t.Fatal("Failed to load mount") + } + if mnt.Path != "/" { + t.Error("Wrong path") + } + if mnt.FilesystemType != "ext4" { + t.Error("Wrong filesystem type") + } + if mnt.DeviceNumber.String() != "259:3" { + t.Error("Wrong device number") + } + if mnt.Subtree != "/" { + t.Error("Wrong subtree") + } + if mnt.ReadOnly { + t.Error("Wrong readonly flag") + } + if len(mountsByPath) != 1 { + t.Error("mountsByPath doesn't contain exactly one entry") + } + if mountsByPath[mnt.Path] != mnt { + t.Error("mountsByPath doesn't contain the correct entry") + } +} + +// Test that Mount.Device is set to the mountpoint's source device if +// applicable, otherwise it is set to the empty string. +func TestLoadSourceDevice(t *testing.T) { + // The mountinfo parser ignores devices that don't exist. For the valid + // device, try /dev/loop0. If it doesn't exist, skip the test. + if _, err := os.Stat("/dev/loop0"); err != nil { + t.Skip("/dev/loop0 does not exist, skipping test") + } + var mountinfo = ` +15 0 7:0 / / rw shared:1 - foo /dev/loop0 rw,data=ordered +31 15 0:27 / /tmp rw,nosuid,nodev shared:17 - tmpfs tmpfs rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("7:0") + if mnt.Device != "/dev/loop0" { + t.Error("mnt.Device wasn't set to source device") + } + mnt = mountForDevice("0:27") + if mnt.Device != "" { + t.Error("mnt.Device wasn't set to empty string for an invalid device") + } +} + +// Test that non-directory mounts are ignored. +func TestNondirectoryMountsIgnored(t *testing.T) { + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + file, err := os.CreateTemp("", "fscrypt_regfile") + if err != nil { + t.Fatal(err) + } + file.Close() + defer os.Remove(file.Name()) + + mountinfo := fmt.Sprintf("15 0 259:3 /foo %s rw,relatime shared:1 - ext4 /dev/root rw", file.Name()) + loadMountInfoFromString(mountinfo) + if len(mountsByDevice) != 0 { + t.Error("Non-directory mount wasn't ignored") + } +} + +// Test that when multiple mounts are on one directory, the last is the one +// which is kept. +func TestNonLatestMountsIgnored(t *testing.T) { + mountinfo := ` +15 0 259:3 / / rw shared:1 - ext4 /dev/root rw +15 0 259:3 / / rw shared:1 - f2fs /dev/root rw +15 0 259:3 / / rw shared:1 - ubifs /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.FilesystemType != "ubifs" { + t.Error("Last mount didn't supersede previous ones") + } +} + +// Test that escape sequences in the mountinfo file are unescaped correctly. +func TestLoadMountWithSpecialCharacters(t *testing.T) { + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempDir, err = filepath.Abs(tempDir) + if err != nil { + t.Fatal(err) + } + mountpoint := filepath.Join(tempDir, "/My Directory\t\n\\") + if err := os.Mkdir(mountpoint, 0700); err != nil { + t.Fatal(err) + } + mountinfo := fmt.Sprintf("15 0 259:3 / %s/My\\040Directory\\011\\012\\134 rw shared:1 - ext4 /dev/root rw", tempDir) + + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.Path != mountpoint { + t.Fatal("Wrong mountpoint") + } +} + +// Tests the EscapeString() and unescapeString() functions. +func TestStringEscaping(t *testing.T) { + charsNeedEscaping := " \t\n\\" + charsDontNeedEscaping := "ABCDEF\u2603\xff\xff\v" + + orig := charsNeedEscaping + charsDontNeedEscaping + escaped := `\040\011\012\134` + charsDontNeedEscaping + if EscapeString(orig) != escaped { + t.Fatal("EscapeString gave wrong result") + } + if unescapeString(escaped) != orig { + t.Fatal("unescapeString gave wrong result") + } +} + +// Test parsing some invalid mountinfo lines. +func TestLoadBadMountInfo(t *testing.T) { + mountinfos := []string{"a", + "a a a a a a a a a a a a a a a", + "a a a a a a a a a a a a - a a", + "15 0 BAD:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered"} + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + for _, mountinfo := range mountinfos { + loadMountInfoFromString(mountinfo) + if len(mountsByDevice) != 0 { + t.Error("Loaded mount from invalid mountinfo line") + } + } +} + +// Test that the ReadOnly flag is set if the mount is readonly, even if the +// filesystem is read-write. +func TestLoadReadOnlyMount(t *testing.T) { + mountinfo := ` +222 15 259:3 / /mnt ro,relatime shared:1 - ext4 /dev/root rw,data=ordered +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if !mnt.ReadOnly { + t.Error("Wrong readonly flag") + } +} + +// Test that a read-write mount is preferred over a read-only mount. +func TestReadWriteMountIsPreferredOverReadOnlyMount(t *testing.T) { + mountinfo := ` +222 15 259:3 / /home ro shared:1 - ext4 /dev/root rw +222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 / /tmp ro shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.Path != "/mnt" { + t.Error("Wrong mount was chosen") + } +} + +// Test that a mount of the full filesystem is preferred over mounts of non-root +// subtrees, given independent mountpoints. +func TestRootSubtreeIsPreferred(t *testing.T) { + mountinfo := ` +222 15 259:3 /subtree1 /home rw shared:1 - ext4 /dev/root rw +222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /subtree2 /tmp rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.Subtree != "/" { + t.Error("Wrong mount was chosen") + } +} + +// Test that a mount that is not of the full filesystem but still contains all +// other mounted subtrees is preferred, given independent mountpoints. +func TestHighestSubtreeIsPreferred(t *testing.T) { + mountinfo := ` +222 15 259:3 /foo/bar /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /foo /tmp rw shared:1 - ext4 /dev/root rw +222 15 259:3 /foo/baz /home rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/foo" { + t.Error("Wrong mount was chosen") + } +} + +// Test that mountpoint "/" is preferred, given independent subtrees. +func TestRootMountpointIsPreferred(t *testing.T) { + mountinfo := ` +222 15 259:3 /var/cache/pacman/pkg /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /var/lib/lxc/base/rootfs / rw shared:1 - ext4 /dev/root rw +222 15 259:3 /srv/repo/x86_64 /home rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/var/lib/lxc/base/rootfs" { + t.Error("Wrong mount was chosen") + } +} + +// Test that a mountpoint that is not "/" but still contains all other +// mountpoints is preferred, given independent subtrees. +func TestHighestMountpointIsPreferred(t *testing.T) { + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempDir, err = filepath.Abs(tempDir) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil { + t.Fatal(err) + } + mountinfo := fmt.Sprintf(` +222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw +`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c") + + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/1" { + t.Error("Wrong mount was chosen") + } +} + +// Test that if some subtrees are contained in other subtrees, *and* some +// mountpoints are contained in other mountpoints, the chosen Mount is the root +// of a tree of mountpoints whose mounted subtrees contain all mounted subtrees. +func TestLoadContainedSubtreesAndMountpoints(t *testing.T) { + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempDir, err = filepath.Abs(tempDir) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/d", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/e", 0700); err != nil { + t.Fatal(err) + } + // The first three mounts form a tree of mountpoints. The rest have + // independent mountpoints but have mounted subtrees contained in the + // mounted subtrees of the first mountpoint tree. + mountinfo := fmt.Sprintf(` +222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /1/3 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /2/4 %s rw shared:1 - ext4 /dev/root rw +`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c", + tempDir+"/d", tempDir+"/e") + + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/1" { + t.Error("Wrong mount was chosen") + } +} + +// Test loading mounts with independent subtrees *and* independent mountpoints. +// This case is ambiguous, so an explicit nil entry should be stored. +func TestLoadAmbiguousMounts(t *testing.T) { + mountinfo := ` +222 15 259:3 /foo /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /bar /tmp rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt, ok := mountsByDevice[deviceNumber] + if !ok { + t.Error("Entry should exist") + } + if mnt != nil { + t.Error("Entry should be nil") + } +} + +// Test making a filesystem link and following it, and test that leading and +// trailing whitespace in the link is ignored. +func TestGetMountFromLink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + link, err := makeLink(mnt) + if err != nil { + t.Fatal(err) + } + linkedMnt, err := getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + if linkedMnt, err = getMountFromLink(link + "\n"); err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + if linkedMnt, err = getMountFromLink(" " + link + " \r\n"); err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } +} + +// Test that makeLink() is including the expected information in links. +func TestMakeLink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + link, err := makeLink(mnt) + if err != nil { + t.Fatal(err) + } + + // Normally, both UUID and PATH should be included. + if !strings.Contains(link, "UUID=") { + t.Fatal("Link doesn't contain UUID") + } + if !strings.Contains(link, "PATH=") { + t.Fatal("Link doesn't contain PATH") + } + + // Without a valid device number, only PATH should be included. + mntCopy := *mnt + mntCopy.DeviceNumber = 0 + link, err = makeLink(&mntCopy) + if err != nil { + t.Fatal(err) + } + if strings.Contains(link, "UUID=") { + t.Fatal("Link shouldn't contain UUID") + } + if !strings.Contains(link, "PATH=") { + t.Fatal("Link doesn't contain PATH") + } +} + +// Test that old filesystem links that contain a UUID only still work. +func TestGetMountFromLegacyLink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + uuid, err := mnt.getFilesystemUUID() + if uuid == "" || err != nil { + t.Fatal("Can't get UUID of test filesystem") + } + + link := fmt.Sprintf("UUID=%s", uuid) + linkedMnt, err := getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } +} + +// Test that if the UUID in a filesystem link doesn't work, then the PATH is +// used instead, and vice versa. +func TestGetMountFromLinkFallback(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + badUUID := "00000000-0000-0000-0000-000000000000" + badPath := "/NONEXISTENT_MOUNT" + goodUUID, err := mnt.getFilesystemUUID() + if goodUUID == "" || err != nil { + t.Fatal("Can't get UUID of test filesystem") + } + + // only PATH valid (should succeed) + link := fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, mnt.Path) + linkedMnt, err := getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + + // only PATH given at all (should succeed) + link = fmt.Sprintf("PATH=%s\n", mnt.Path) + linkedMnt, err = getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + + // only UUID valid (should succeed) + link = fmt.Sprintf("UUID=%s\nPATH=%s\n", goodUUID, badPath) + if linkedMnt, err = getMountFromLink(link); err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + + // neither valid (should fail) + link = fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, badPath) + linkedMnt, err = getMountFromLink(link) + if linkedMnt != nil || err == nil { + t.Fatal("Following a bad link succeeded") + } +} + // Benchmarks how long it takes to update the mountpoint data func BenchmarkLoadFirst(b *testing.B) { for n := 0; n < b.N; n++ { @@ -38,3 +545,21 @@ func BenchmarkLoadFirst(b *testing.B) { } } } + +// Test mount comparison by values instead of by reference, +// as the map mountsByDevice gets recreated on each load. +// This ensures that concurrent mounts work properly. +func TestMountComparison(t *testing.T) { + var mountinfo = ` +15 0 259:3 / /home rw,relatime shared:1 - ext4 /dev/root rw,data=ordered +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + firstMnt := mountForDevice("259:3") + loadMountInfoFromString(mountinfo) + secondMnt := mountForDevice("259:3") + if !reflect.DeepEqual(firstMnt, secondMnt) { + t.Errorf("Mount comparison does not work") + } +} diff --git a/filesystem/path.go b/filesystem/path.go index d788a6b..8cfb235 100644 --- a/filesystem/path.go +++ b/filesystem/path.go @@ -20,18 +20,26 @@ package filesystem import ( + "fmt" "log" "os" "path/filepath" + "golang.org/x/sys/unix" + "github.com/pkg/errors" ) -// We only check the unix permissions and the sticky bit -const permMask = os.ModeSticky | os.ModePerm +// OpenFileOverridingUmask calls os.OpenFile but with the umask overridden so +// that no permission bits are masked out if the file is created. +func OpenFileOverridingUmask(name string, flag int, perm os.FileMode) (*os.File, error) { + oldMask := unix.Umask(0) + defer unix.Umask(oldMask) + return os.OpenFile(name, flag, perm) +} -// cannonicalizePath turns path into an absolute path without symlinks. -func cannonicalizePath(path string) (string, error) { +// canonicalizePath turns path into an absolute path without symlinks. +func canonicalizePath(path string) (string, error) { path, err := filepath.Abs(path) if err != nil { return "", err @@ -56,36 +64,65 @@ func loggedStat(name string) (os.FileInfo, error) { return info, err } +// loggedLstat runs os.Lstat (doesn't dereference trailing symlink), but it logs +// the error if lstat returns any error other than nil or IsNotExist. +func loggedLstat(name string) (os.FileInfo, error) { + info, err := os.Lstat(name) + if err != nil && !os.IsNotExist(err) { + log.Print(err) + } + return info, err +} + // isDir returns true if the path exists and is that of a directory. func isDir(path string) bool { info, err := loggedStat(path) return err == nil && info.IsDir() } -// isDevice returns true if the path exists and is that of a directory. -func isDevice(path string) bool { +// isRegularFile returns true if the path exists and is that of a regular file. +func isRegularFile(path string) bool { info, err := loggedStat(path) - return err == nil && info.Mode()&os.ModeDevice != 0 + return err == nil && info.Mode().IsRegular() } -// isDirCheckPerm returns true if the path exists and is a directory. If the -// specified permissions and sticky bit of mode do not match the path, and error -// is logged. -func isDirCheckPerm(path string, mode os.FileMode) bool { - info, err := loggedStat(path) - // Check if directory - if err != nil || !info.IsDir() { - return false +// HaveReadAccessTo returns true if the process has read access to a file or +// directory, without actually opening it. +func HaveReadAccessTo(path string) bool { + return unix.Access(path, unix.R_OK) == nil +} + +// DeviceNumber represents a combined major:minor device number. +type DeviceNumber uint64 + +func (num DeviceNumber) String() string { + return fmt.Sprintf("%d:%d", unix.Major(uint64(num)), unix.Minor(uint64(num))) +} + +func newDeviceNumberFromString(str string) (DeviceNumber, error) { + var major, minor uint32 + if count, _ := fmt.Sscanf(str, "%d:%d", &major, &minor); count != 2 { + return 0, errors.Errorf("invalid device number string %q", str) } - // Check for bad permissions - if info.Mode()&permMask != mode&permMask { - log.Printf("directory %s has incorrect permissions", path) + return DeviceNumber(unix.Mkdev(major, minor)), nil +} + +// getDeviceNumber returns the device number of the device node at the given +// path. If there is a symlink at the path, it is dereferenced. +func getDeviceNumber(path string) (DeviceNumber, error) { + var stat unix.Stat_t + if err := unix.Stat(path, &stat); err != nil { + return 0, err } - return true + return DeviceNumber(stat.Rdev), nil } -// isRegularFile returns true if the path exists and is that of a regular file. -func isRegularFile(path string) bool { - info, err := loggedStat(path) - return err == nil && info.Mode().IsRegular() +// getNumberOfContainingDevice returns the device number of the filesystem which +// contains the given file. If the file is a symlink, it is not dereferenced. +func getNumberOfContainingDevice(path string) (DeviceNumber, error) { + var stat unix.Stat_t + if err := unix.Lstat(path, &stat); err != nil { + return 0, err + } + return DeviceNumber(stat.Dev), nil } diff --git a/filesystem/path_test.go b/filesystem/path_test.go new file mode 100644 index 0000000..d325054 --- /dev/null +++ b/filesystem/path_test.go @@ -0,0 +1,85 @@ +/* + * path_test.go - Tests for path utilities. + * + * Copyright 2019 Google LLC + * + * 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" + "os" + "testing" + + "github.com/google/fscrypt/util" +) + +func TestDeviceNumber(t *testing.T) { + num, err := getDeviceNumber("/NONEXISTENT") + if num != 0 || err == nil { + t.Error("Should have failed to get device number of nonexistent file") + } + // /dev/null is always device 1:3 on Linux. + num, err = getDeviceNumber("/dev/null") + if err != nil { + t.Fatal(err) + } + if str := num.String(); str != "1:3" { + t.Errorf("Wrong device number string: %q", str) + } + if str := fmt.Sprintf("%v", num); str != "1:3" { + t.Errorf("Wrong device number string: %q", str) + } + var num2 DeviceNumber + num2, err = newDeviceNumberFromString("1:3") + if err != nil { + t.Error("Failed to parse device number") + } + if num2 != num { + t.Errorf("Wrong device number: %d", num2) + } + num2, err = newDeviceNumberFromString("foo") + if num2 != 0 || err == nil { + t.Error("Should have failed to parse invalid device number") + } +} + +func TestHaveReadAccessTo(t *testing.T) { + if util.IsUserRoot() { + t.Skip("This test cannot be run as root") + } + file, err := os.CreateTemp("", "fscrypt_test") + if err != nil { + t.Fatal(err) + } + file.Close() + defer os.Remove(file.Name()) + + testCases := map[os.FileMode]bool{ + 0444: true, + 0400: true, + 0000: false, + 0040: false, // user bits take priority in Linux + 0004: false, // user bits take priority in Linux + } + for mode, readable := range testCases { + if err := os.Chmod(file.Name(), mode); err != nil { + t.Error(err) + } + if HaveReadAccessTo(file.Name()) != readable { + t.Errorf("Expected readable=%v on mode=0%03o", readable, mode) + } + } +} |