diff --git a/src/lib/file-create-locked.c b/src/lib/file-create-locked.c index 57ee123ba8..2ac30c1658 100644 --- a/src/lib/file-create-locked.c +++ b/src/lib/file-create-locked.c @@ -3,6 +3,7 @@ #include "lib.h" #include "str.h" #include "safe-mkstemp.h" +#include "mkdir-parents.h" #include "file-lock.h" #include "file-create-locked.h" @@ -10,6 +11,10 @@ #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 @@ -43,25 +48,62 @@ try_lock_existing(int fd, const char *path, 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) { + *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 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); - 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); + 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; } + + ret = -1; if (file_try_lock_error(fd, str_c(temp_path), F_WRLCK, set->lock_method, lock_r, error_r) <= 0) { } else if (link(str_c(temp_path), path) < 0) { diff --git a/src/lib/file-create-locked.h b/src/lib/file-create-locked.h index 29bbb8144b..cba4476707 100644 --- a/src/lib/file-create-locked.h +++ b/src/lib/file-create-locked.h @@ -15,6 +15,17 @@ struct file_create_settings { /* 0 = default */ gid_t gid; const char *gid_origin; + + /* If parent directory doesn't exist, mkdir() it with this mode. + 0 = don't mkdir(). The parent directories are assumed to be + potentially rmdir() simultaneously, so the mkdir()+locking may be + attempted multiple times. */ + int mkdir_mode; + /* 0 = default */ + uid_t mkdir_uid; + /* 0 = default */ + gid_t mkdir_gid; + const char *mkdir_gid_origin; }; /* Either open an existing file and lock it, or create the file locked. diff --git a/src/lib/test-file-create-locked.c b/src/lib/test-file-create-locked.c index d604458915..0b16819c19 100644 --- a/src/lib/test-file-create-locked.c +++ b/src/lib/test-file-create-locked.c @@ -1,6 +1,7 @@ /* Copyright (c) 2017 Dovecot authors, see the included COPYING file */ #include "test-lib.h" +#include "unlink-directory.h" #include "file-create-locked.h" #include @@ -85,7 +86,51 @@ static void test_file_create_locked_basic(void) test_end(); } +static void test_file_create_locked_mkdir(void) +{ + struct file_create_settings set = { + .lock_timeout_secs = 0, + .lock_method = FILE_LOCK_METHOD_FCNTL, + }; + const char *path = ".test-file-create-locked"; + struct file_lock *lock; + const char *error, *dir; + bool created; + int fd; + + test_begin("file_create_locked() with mkdir"); + + dir = ".test-temp-file-create-locked-dir"; + if (unlink_directory(dir, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0) + i_fatal("unlink_directory(%s) failed: %s", dir, error); + path = t_strconcat(dir, "/lockfile", NULL); + + /* try without mkdir enabled */ + test_assert(file_create_locked(path, &set, &lock, &created, &error) == -1); + test_assert(errno == ENOENT); + + /* try with mkdir enabled */ + set.mkdir_mode = 0700; + fd = file_create_locked(path, &set, &lock, &created, &error); + test_assert(fd > 0); + test_assert(created); + i_close_fd(&fd); + + struct stat st; + if (stat(dir, &st) < 0) + i_error("stat(%s) failed: %m", dir); + test_assert((st.st_mode & 0777) == 0700); + i_unlink(path); + file_lock_free(&lock); + + if (unlink_directory(dir, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0) + i_fatal("unlink_directory(%s) failed: %s", dir, error); + + test_end(); +} + void test_file_create_locked(void) { test_file_create_locked_basic(); + test_file_create_locked_mkdir(); }