diff options
Diffstat (limited to 'src/lib/file-create-locked.c')
-rw-r--r-- | src/lib/file-create-locked.c | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/src/lib/file-create-locked.c b/src/lib/file-create-locked.c new file mode 100644 index 0000000..47fe7f9 --- /dev/null +++ b/src/lib/file-create-locked.c @@ -0,0 +1,193 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "safe-mkstemp.h" +#include "mkdir-parents.h" +#include "file-lock.h" +#include "file-create-locked.h" + +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> + +/* Try mkdir() + lock creation multiple times. This allows the lock file + creation to work even while the directory is simultaneously being + rmdir()ed. */ +#define MAX_MKDIR_COUNT 10 +#define MAX_RETRY_COUNT 1000 + +static int +try_lock_existing(int fd, const char *path, + const struct file_create_settings *set, + struct file_lock **lock_r, const char **error_r) +{ + struct file_lock_settings lock_set = set->lock_settings; + struct stat st1, st2; + int ret; + + lock_set.unlink_on_free = FALSE; + lock_set.close_on_free = FALSE; + + if (fstat(fd, &st1) < 0) { + *error_r = t_strdup_printf("fstat(%s) failed: %m", path); + return -1; + } + if (file_wait_lock(fd, path, F_WRLCK, &lock_set, set->lock_timeout_secs, + lock_r, error_r) <= 0) + return -1; + if (stat(path, &st2) == 0) { + ret = st1.st_ino == st2.st_ino && + CMP_DEV_T(st1.st_dev, st2.st_dev) ? 1 : 0; + } else if (errno == ENOENT) { + ret = 0; + } else { + *error_r = t_strdup_printf("stat(%s) failed: %m", path); + ret = -1; + } + if (ret <= 0) { + /* the fd is closed next - no need to unlock */ + file_lock_free(lock_r); + } else { + file_lock_set_unlink_on_free( + *lock_r, set->lock_settings.unlink_on_free); + file_lock_set_close_on_free( + *lock_r, set->lock_settings.close_on_free); + } + return ret; +} + +static int +try_mkdir(const char *path, const struct file_create_settings *set, + const char **error_r) +{ + uid_t uid = set->mkdir_uid != 0 ? set->mkdir_uid : (uid_t)-1; + gid_t gid = set->mkdir_gid != 0 ? set->mkdir_gid : (gid_t)-1; + const char *p = strrchr(path, '/'); + if (p == NULL) + return 0; + + const char *dir = t_strdup_until(path, p); + int ret; + if (uid != (uid_t)-1) + ret = mkdir_parents_chown(dir, set->mkdir_mode, uid, gid); + else { + ret = mkdir_parents_chgrp(dir, set->mkdir_mode, + gid, set->gid_origin); + } + if (ret < 0 && errno != EEXIST) { + *error_r = t_strdup_printf("mkdir_parents(%s) failed: %m", dir); + return -1; + } + return 1; +} + +static int +try_create_new(const char *path, const struct file_create_settings *set, + int *fd_r, struct file_lock **lock_r, const char **error_r) +{ + string_t *temp_path = t_str_new(128); + int fd, orig_errno, ret = 1; + int mode = set->mode != 0 ? set->mode : 0600; + uid_t uid = set->uid != 0 ? set->uid : (uid_t)-1; + uid_t gid = set->gid != 0 ? set->gid : (gid_t)-1; + + str_append(temp_path, path); + for (unsigned int i = 0; ret > 0; i++) { + if (uid != (uid_t)-1) + fd = safe_mkstemp(temp_path, mode, uid, gid); + else + fd = safe_mkstemp_group(temp_path, mode, gid, set->gid_origin); + if (fd != -1 || errno != ENOENT || set->mkdir_mode == 0 || + i >= MAX_MKDIR_COUNT) + break; + + int orig_errno = errno; + if ((ret = try_mkdir(path, set, error_r)) < 0) + return -1; + errno = orig_errno; + } + if (fd == -1) { + *error_r = t_strdup_printf("safe_mkstemp(%s) failed: %m", path); + return -1; + } + + struct file_lock_settings lock_set = set->lock_settings; + lock_set.unlink_on_free = FALSE; + lock_set.close_on_free = FALSE; + + ret = -1; + if (file_try_lock(fd, str_c(temp_path), F_WRLCK, &lock_set, + lock_r, error_r) <= 0) { + } else if (link(str_c(temp_path), path) < 0) { + if (errno == EEXIST) { + /* just created by somebody else */ + ret = 0; + } else if (errno == ENOENT) { + /* nobody should be deleting the temp file unless the + entire directory is deleted. */ + *error_r = t_strdup_printf( + "Temporary file %s was unexpectedly deleted", + str_c(temp_path)); + } else { + *error_r = t_strdup_printf("link(%s, %s) failed: %m", + str_c(temp_path), path); + } + file_lock_free(lock_r); + } else { + file_lock_set_path(*lock_r, path); + file_lock_set_unlink_on_free( + *lock_r, set->lock_settings.unlink_on_free); + file_lock_set_close_on_free( + *lock_r, set->lock_settings.close_on_free); + i_unlink_if_exists(str_c(temp_path)); + *fd_r = fd; + return 1; + } + orig_errno = errno; + i_close_fd(&fd); + i_unlink_if_exists(str_c(temp_path)); + errno = orig_errno; + return ret; +} + +int file_create_locked(const char *path, const struct file_create_settings *set, + struct file_lock **lock_r, bool *created_r, + const char **error_r) +{ + unsigned int i; + int fd, ret; + + for (i = 0; i < MAX_RETRY_COUNT; i++) { + fd = open(path, O_RDWR); + if (fd != -1) { + ret = try_lock_existing(fd, path, set, lock_r, error_r); + if (ret > 0) { + /* successfully locked an existing file */ + *created_r = FALSE; + return fd; + } + i_close_fd(&fd); + if (ret < 0) + return -1; + } else if (errno != ENOENT) { + *error_r = t_strdup_printf("open(%s) failed: %m", path); + return -1; + } else { + /* try to create the file */ + ret = try_create_new(path, set, &fd, lock_r, error_r); + if (ret < 0) + return -1; + if (ret > 0) { + /* successfully created a new locked file */ + *created_r = TRUE; + return fd; + } + /* the file was just created - try again opening and + locking it */ + } + } + *error_r = t_strdup_printf("Creating a locked file %s keeps failing", path); + errno = EINVAL; + return -1; +} |