summaryrefslogtreecommitdiffstats
path: root/src/lib/file-create-locked.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/file-create-locked.c')
-rw-r--r--src/lib/file-create-locked.c193
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;
+}