/* 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 #include #include /* 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; }