diff options
Diffstat (limited to 'lib/utils_device_locking.c')
-rw-r--r-- | lib/utils_device_locking.c | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/lib/utils_device_locking.c b/lib/utils_device_locking.c new file mode 100644 index 0000000..a8becc4 --- /dev/null +++ b/lib/utils_device_locking.c @@ -0,0 +1,345 @@ +/* + * Metadata on-disk locking for processes serialization + * + * Copyright (C) 2016-2019 Red Hat, Inc. All rights reserved. + * Copyright (C) 2016-2019 Ondrej Kozina + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <errno.h> +#include <fcntl.h> +#include <linux/limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/file.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#ifdef HAVE_SYS_SYSMACROS_H +# include <sys/sysmacros.h> /* for major, minor */ +#endif +#include <libgen.h> + +#include "internal.h" +#include "utils_device_locking.h" + +#define same_inode(buf1, buf2) \ + ((buf1).st_ino == (buf2).st_ino && \ + (buf1).st_dev == (buf2).st_dev) + +#ifndef __GNUC__ +# define __typeof__ typeof +#endif + +enum lock_type { + DEV_LOCK_READ = 0, + DEV_LOCK_WRITE +}; + +struct crypt_lock_handle { + dev_t devno; + int flock_fd; + enum lock_type type; + __typeof__( ((struct stat*)0)->st_mode) mode; +}; + +static int resource_by_devno(char *res, size_t res_size, dev_t devno, unsigned fullpath) +{ + int r; + + if (fullpath) + r = snprintf(res, res_size, "%s/L_%d:%d", DEFAULT_LUKS2_LOCK_PATH, major(devno), minor(devno)); + else + r = snprintf(res, res_size, "L_%d:%d", major(devno), minor(devno)); + + return (r < 0 || (size_t)r >= res_size) ? -EINVAL : 0; +} + +static int open_lock_dir(struct crypt_device *cd, const char *dir, const char *base) +{ + int dirfd, lockdfd; + + dirfd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (dirfd < 0) { + log_dbg(cd, "Failed to open directory %s: (%d: %s).", dir, errno, strerror(errno)); + if (errno == ENOTDIR || errno == ENOENT) + log_err(cd, _("Locking aborted. The locking path %s/%s is unusable (not a directory or missing)."), dir, base); + return -EINVAL; + } + + lockdfd = openat(dirfd, base, O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + if (lockdfd < 0) { + if (errno == ENOENT) { + log_std(cd, _("WARNING: Locking directory %s/%s is missing!\n"), dir, base); + + /* success or failure w/ errno == EEXIST either way just try to open the 'base' directory again */ + if (mkdirat(dirfd, base, DEFAULT_LUKS2_LOCK_DIR_PERMS) && errno != EEXIST) + log_dbg(cd, "Failed to create directory %s in %s (%d: %s).", base, dir, errno, strerror(errno)); + else + lockdfd = openat(dirfd, base, O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + } else { + log_dbg(cd, "Failed to open directory %s/%s: (%d: %s)", dir, base, errno, strerror(errno)); + if (errno == ENOTDIR || errno == ELOOP) + log_err(cd, _("Locking aborted. The locking path %s/%s is unusable (%s is not a directory)."), dir, base, base); + } + } + + close(dirfd); + return lockdfd >= 0 ? lockdfd : -EINVAL; +} + +static int open_resource(struct crypt_device *cd, const char *res) +{ + int err, lockdir_fd, r; + char dir[] = DEFAULT_LUKS2_LOCK_PATH, + base[] = DEFAULT_LUKS2_LOCK_PATH; + + lockdir_fd = open_lock_dir(cd, dirname(dir), basename(base)); + if (lockdir_fd < 0) + return -EINVAL; + + log_dbg(cd, "Opening lock resource file %s/%s", DEFAULT_LUKS2_LOCK_PATH, res); + r = openat(lockdir_fd, res, O_CREAT | O_NOFOLLOW | O_RDWR | O_CLOEXEC, 0777); + err = errno; + + close(lockdir_fd); + + return r < 0 ? -err : r; +} + +static int acquire_lock_handle(struct crypt_device *cd, const char *device_path, struct crypt_lock_handle *h) +{ + char res[PATH_MAX]; + int dev_fd, fd; + struct stat st; + + dev_fd = open(device_path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (dev_fd < 0) + return -EINVAL; + + if (fstat(dev_fd, &st)) { + close(dev_fd); + return -EINVAL; + } + + if (S_ISBLK(st.st_mode)) { + if (resource_by_devno(res, sizeof(res), st.st_rdev, 0)) { + close(dev_fd); + return -EINVAL; + } + + fd = open_resource(cd, res); + close(dev_fd); + if (fd < 0) + return fd; + + h->flock_fd = fd; + h->devno = st.st_rdev; + } else if (S_ISREG(st.st_mode)) { + // FIXME: workaround for nfsv4 + fd = open(device_path, O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) + h->flock_fd = dev_fd; + else { + h->flock_fd = fd; + close(dev_fd); + } + } else { + /* Wrong device type */ + close(dev_fd); + return -EINVAL; + } + + h->mode = st.st_mode; + + return 0; +} + +static void release_lock_handle(struct crypt_device *cd, struct crypt_lock_handle *h) +{ + char res[PATH_MAX]; + struct stat buf_a, buf_b; + + if (S_ISBLK(h->mode) && /* was it block device */ + !flock(h->flock_fd, LOCK_EX | LOCK_NB) && /* lock to drop the file */ + !resource_by_devno(res, sizeof(res), h->devno, 1) && /* acquire lock resource name */ + !fstat(h->flock_fd, &buf_a) && /* read inode id referred by fd */ + !stat(res, &buf_b) && /* does path file still exist? */ + same_inode(buf_a, buf_b)) { /* is it same id as the one referenced by fd? */ + /* coverity[toctou] */ + if (unlink(res)) /* yes? unlink the file */ + log_dbg(cd, "Failed to unlink resource file: %s", res); + } + + if (close(h->flock_fd)) + log_dbg(cd, "Failed to close resource fd (%d).", h->flock_fd); +} + +int device_locked(struct crypt_lock_handle *h) +{ + return (h && (h->type == DEV_LOCK_READ || h->type == DEV_LOCK_WRITE)); +} + +int device_locked_readonly(struct crypt_lock_handle *h) +{ + return (h && h->type == DEV_LOCK_READ); +} + +static int verify_lock_handle(const char *device_path, struct crypt_lock_handle *h) +{ + char res[PATH_MAX]; + struct stat lck_st, res_st; + + /* we locked a regular file, check during device_open() instead. No reason to check now */ + if (S_ISREG(h->mode)) + return 0; + + if (resource_by_devno(res, sizeof(res), h->devno, 1)) + return -EINVAL; + + if (fstat(h->flock_fd, &lck_st)) + return -EINVAL; + + return (stat(res, &res_st) || !same_inode(lck_st, res_st)) ? -EAGAIN : 0; +} + +struct crypt_lock_handle *device_read_lock_handle(struct crypt_device *cd, const char *device_path) +{ + int r; + struct crypt_lock_handle *h = malloc(sizeof(*h)); + + if (!h) + return NULL; + + do { + r = acquire_lock_handle(cd, device_path, h); + if (r) + break; + + log_dbg(cd, "Acquiring read lock for device %s.", device_path); + + if (flock(h->flock_fd, LOCK_SH)) { + log_dbg(cd, "Shared flock failed with errno %d.", errno); + r = -EINVAL; + release_lock_handle(cd, h); + break; + } + + log_dbg(cd, "Verifying read lock handle for device %s.", device_path); + + /* + * check whether another libcryptsetup process removed resource file before this + * one managed to flock() it. See release_lock_handle() for details + */ + r = verify_lock_handle(device_path, h); + if (r) { + flock(h->flock_fd, LOCK_UN); + release_lock_handle(cd, h); + log_dbg(cd, "Read lock handle verification failed."); + } + } while (r == -EAGAIN); + + if (r) { + free(h); + return NULL; + } + + h->type = DEV_LOCK_READ; + + return h; +} + +struct crypt_lock_handle *device_write_lock_handle(struct crypt_device *cd, const char *device_path) +{ + int r; + struct crypt_lock_handle *h = malloc(sizeof(*h)); + + if (!h) + return NULL; + + do { + r = acquire_lock_handle(cd, device_path, h); + if (r) + break; + + log_dbg(cd, "Acquiring write lock for device %s.", device_path); + + if (flock(h->flock_fd, LOCK_EX)) { + log_dbg(cd, "Exclusive flock failed with errno %d.", errno); + r = -EINVAL; + release_lock_handle(cd, h); + break; + } + + log_dbg(cd, "Verifying write lock handle for device %s.", device_path); + + /* + * check whether another libcryptsetup process removed resource file before this + * one managed to flock() it. See release_lock_handle() for details + */ + r = verify_lock_handle(device_path, h); + if (r) { + flock(h->flock_fd, LOCK_UN); + release_lock_handle(cd, h); + log_dbg(cd, "Write lock handle verification failed."); + } + } while (r == -EAGAIN); + + if (r) { + free(h); + return NULL; + } + + h->type = DEV_LOCK_WRITE; + + return h; +} + +void device_unlock_handle(struct crypt_device *cd, struct crypt_lock_handle *h) +{ + if (flock(h->flock_fd, LOCK_UN)) + log_dbg(cd, "flock on fd %d failed.", h->flock_fd); + + release_lock_handle(cd, h); + + free(h); +} + +int device_locked_verify(struct crypt_device *cd, int dev_fd, struct crypt_lock_handle *h) +{ + char res[PATH_MAX]; + struct stat dev_st, lck_st, st; + + if (fstat(dev_fd, &dev_st) || fstat(h->flock_fd, &lck_st)) + return 1; + + /* if device handle is regular file the handle must match the lock handle */ + if (S_ISREG(dev_st.st_mode)) { + log_dbg(cd, "Veryfing locked device handle (regular file)"); + if (!same_inode(dev_st, lck_st)) + return 1; + } else if (S_ISBLK(dev_st.st_mode)) { + log_dbg(cd, "Veryfing locked device handle (bdev)"); + if (resource_by_devno(res, sizeof(res), dev_st.st_rdev, 1) || + stat(res, &st) || + !same_inode(lck_st, st)) + return 1; + } else + return 1; + + return 0; +} |