aboutsummaryrefslogtreecommitdiff
path: root/filesystem
diff options
context:
space:
mode:
Diffstat (limited to 'filesystem')
-rw-r--r--filesystem/filesystem.go958
-rw-r--r--filesystem/filesystem_test.go371
-rw-r--r--filesystem/mountpoint.go596
-rw-r--r--filesystem/mountpoint_test.go525
-rw-r--r--filesystem/path.go83
-rw-r--r--filesystem/path_test.go85
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)
+ }
+ }
+}