aboutsummaryrefslogtreecommitdiff
path: root/filesystem/filesystem.go
diff options
context:
space:
mode:
Diffstat (limited to 'filesystem/filesystem.go')
-rw-r--r--filesystem/filesystem.go958
1 files changed, 775 insertions, 183 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
+}