/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "istream.h" #include "file-lock.h" #include "file-dotlock.h" #include "time-util.h" #include #include #ifdef HAVE_FLOCK # include #endif struct file_lock { struct file_lock_settings set; int fd; char *path; struct dotlock *dotlock; struct timeval locked_time; int lock_type; }; static struct timeval lock_wait_start; static uint64_t file_lock_wait_usecs = 0; static long long file_lock_slow_warning_usecs = -1; static void file_lock_log_warning_if_slow(struct file_lock *lock); bool file_lock_method_parse(const char *name, enum file_lock_method *method_r) { if (strcasecmp(name, "fcntl") == 0) *method_r = FILE_LOCK_METHOD_FCNTL; else if (strcasecmp(name, "flock") == 0) *method_r = FILE_LOCK_METHOD_FLOCK; else if (strcasecmp(name, "dotlock") == 0) *method_r = FILE_LOCK_METHOD_DOTLOCK; else return FALSE; return TRUE; } const char *file_lock_method_to_str(enum file_lock_method method) { switch (method) { case FILE_LOCK_METHOD_FCNTL: return "fcntl"; case FILE_LOCK_METHOD_FLOCK: return "flock"; case FILE_LOCK_METHOD_DOTLOCK: return "dotlock"; } i_unreached(); } int file_try_lock(int fd, const char *path, int lock_type, const struct file_lock_settings *set, struct file_lock **lock_r, const char **error_r) { return file_wait_lock(fd, path, lock_type, set, 0, lock_r, error_r); } static const char * file_lock_find_fcntl(int lock_fd, int lock_type) { struct flock fl; i_zero(&fl); fl.l_type = lock_type; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0; if (fcntl(lock_fd, F_GETLK, &fl) < 0 || fl.l_type == F_UNLCK || fl.l_pid == -1 || fl.l_pid == 0) return ""; return t_strdup_printf(" (%s lock held by pid %ld)", fl.l_type == F_RDLCK ? "READ" : "WRITE", (long)fl.l_pid); } static const char * file_lock_find_proc_locks(int lock_fd ATTR_UNUSED) { /* do anything except Linux support this? don't bother trying it for OSes we don't know about. */ #ifdef __linux__ static bool have_proc_locks = TRUE; struct stat st; char node_buf[MAX_INT_STRLEN * 3 + 2]; struct istream *input; const char *line, *lock_type = ""; pid_t pid = 0; int fd; if (!have_proc_locks) return NULL; if (fstat(lock_fd, &st) < 0) return ""; i_snprintf(node_buf, sizeof(node_buf), "%02x:%02x:%llu", major(st.st_dev), minor(st.st_dev), (unsigned long long)st.st_ino); fd = open("/proc/locks", O_RDONLY); if (fd == -1) { have_proc_locks = FALSE; return ""; } input = i_stream_create_fd_autoclose(&fd, 512); while (pid == 0 && (line = i_stream_read_next_line(input)) != NULL) T_BEGIN { const char *const *args = t_strsplit_spaces(line, " "); /* number: FLOCK/POSIX ADVISORY READ/WRITE pid major:minor:inode region-start region-end */ if (str_array_length(args) < 8) { ; /* don't continue from within a T_BEGIN {...} T_END */ } else if (strcmp(args[5], node_buf) == 0) { lock_type = strcmp(args[3], "READ") == 0 ? "READ" : "WRITE"; if (str_to_pid(args[4], &pid) < 0) pid = 0; } } T_END; i_stream_destroy(&input); if (pid == 0) { /* not found */ return ""; } if (pid == getpid()) return " (BUG: lock is held by our own process)"; return t_strdup_printf(" (%s lock held by pid %ld)", lock_type, (long)pid); #else return ""; #endif } const char *file_lock_find(int lock_fd, enum file_lock_method lock_method, int lock_type) { const char *ret; if (lock_method == FILE_LOCK_METHOD_FCNTL) { ret = file_lock_find_fcntl(lock_fd, lock_type); if (ret[0] != '\0') return ret; } return file_lock_find_proc_locks(lock_fd); } static bool err_is_lock_timeout(time_t started, unsigned int timeout_secs) { /* if EINTR took at least timeout_secs-1 number of seconds, assume it was the alarm. otherwise log EINTR failure. (We most likely don't want to retry EINTR since a signal means somebody wants us to stop blocking). */ return errno == EINTR && (unsigned long)(time(NULL) - started + 1) >= timeout_secs; } static int file_lock_do(int fd, const char *path, int lock_type, const struct file_lock_settings *set, unsigned int timeout_secs, const char **error_r) { const char *lock_type_str; time_t started = time(NULL); int ret; i_assert(fd != -1); if (timeout_secs != 0) { alarm(timeout_secs); file_lock_wait_start(); } lock_type_str = lock_type == F_UNLCK ? "unlock" : (lock_type == F_RDLCK ? "read-lock" : "write-lock"); switch (set->lock_method) { case FILE_LOCK_METHOD_FCNTL: { #ifndef HAVE_FCNTL *error_r = t_strdup_printf( "Can't lock file %s: fcntl() locks not supported", path); return -1; #else struct flock fl; fl.l_type = lock_type; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0; ret = fcntl(fd, timeout_secs != 0 ? F_SETLKW : F_SETLK, &fl); if (timeout_secs != 0) { alarm(0); file_lock_wait_end(path); } if (ret == 0) break; if (timeout_secs == 0 && (errno == EACCES || errno == EAGAIN)) { /* locked by another process */ *error_r = t_strdup_printf( "fcntl(%s, %s, F_SETLK) locking failed: %m " "(File is already locked)", path, lock_type_str); return 0; } if (err_is_lock_timeout(started, timeout_secs)) { errno = EAGAIN; *error_r = t_strdup_printf( "fcntl(%s, %s, F_SETLKW) locking failed: " "Timed out after %u seconds%s", path, lock_type_str, timeout_secs, file_lock_find(fd, set->lock_method, lock_type)); return 0; } *error_r = t_strdup_printf("fcntl(%s, %s, %s) locking failed: %m", path, lock_type_str, timeout_secs == 0 ? "F_SETLK" : "F_SETLKW"); if (errno == EDEADLK && !set->allow_deadlock) { i_panic("%s%s", *error_r, file_lock_find(fd, set->lock_method, lock_type)); } return -1; #endif } case FILE_LOCK_METHOD_FLOCK: { #ifndef HAVE_FLOCK *error_r = t_strdup_printf( "Can't lock file %s: flock() not supported", path); return -1; #else int operation = timeout_secs != 0 ? 0 : LOCK_NB; switch (lock_type) { case F_RDLCK: operation |= LOCK_SH; break; case F_WRLCK: operation |= LOCK_EX; break; case F_UNLCK: operation |= LOCK_UN; break; } ret = flock(fd, operation); if (timeout_secs != 0) { alarm(0); file_lock_wait_end(path); } if (ret == 0) break; if (timeout_secs == 0 && errno == EWOULDBLOCK) { /* locked by another process */ *error_r = t_strdup_printf( "flock(%s, %s) failed: %m " "(File is already locked)", path, lock_type_str); return 0; } if (err_is_lock_timeout(started, timeout_secs)) { errno = EAGAIN; *error_r = t_strdup_printf("flock(%s, %s) failed: " "Timed out after %u seconds%s", path, lock_type_str, timeout_secs, file_lock_find(fd, set->lock_method, lock_type)); return 0; } *error_r = t_strdup_printf("flock(%s, %s) failed: %m", path, lock_type_str); if (errno == EDEADLK && !set->allow_deadlock) { i_panic("%s%s", *error_r, file_lock_find(fd, set->lock_method, lock_type)); } return -1; #endif } case FILE_LOCK_METHOD_DOTLOCK: /* we shouldn't get here */ i_unreached(); } return 1; } int file_wait_lock(int fd, const char *path, int lock_type, const struct file_lock_settings *set, unsigned int timeout_secs, struct file_lock **lock_r, const char **error_r) { struct file_lock *lock; int ret; ret = file_lock_do(fd, path, lock_type, set, timeout_secs, error_r); if (ret <= 0) return ret; lock = i_new(struct file_lock, 1); lock->set = *set; lock->fd = fd; lock->path = i_strdup(path); lock->lock_type = lock_type; i_gettimeofday(&lock->locked_time); *lock_r = lock; return 1; } int file_lock_try_update(struct file_lock *lock, int lock_type) { const char *error; int ret; ret = file_lock_do(lock->fd, lock->path, lock_type, &lock->set, 0, &error); if (ret <= 0) return ret; file_lock_log_warning_if_slow(lock); lock->lock_type = lock_type; return 1; } void file_lock_set_unlink_on_free(struct file_lock *lock, bool set) { lock->set.unlink_on_free = set; } void file_lock_set_close_on_free(struct file_lock *lock, bool set) { lock->set.close_on_free = set; } struct file_lock *file_lock_from_dotlock(struct dotlock **dotlock) { struct file_lock *lock; lock = i_new(struct file_lock, 1); lock->set.lock_method = FILE_LOCK_METHOD_DOTLOCK; lock->fd = -1; lock->path = i_strdup(file_dotlock_get_lock_path(*dotlock)); lock->lock_type = F_WRLCK; i_gettimeofday(&lock->locked_time); lock->dotlock = *dotlock; *dotlock = NULL; return lock; } static void file_unlock_real(struct file_lock *lock) { const char *error; if (file_lock_do(lock->fd, lock->path, F_UNLCK, &lock->set, 0, &error) == 0) { /* this shouldn't happen */ i_error("file_unlock(%s) failed: %m", lock->path); } } void file_unlock(struct file_lock **_lock) { struct file_lock *lock = *_lock; *_lock = NULL; /* unlocking is unnecessary when the file is unlinked. or alternatively the unlink() must be done before unlocking, because otherwise it could be deleting the new lock. */ i_assert(!lock->set.unlink_on_free); if (lock->dotlock == NULL) file_unlock_real(lock); file_lock_free(&lock); } static void file_try_unlink_locked(struct file_lock *lock) { struct file_lock *temp_lock = NULL; struct file_lock_settings temp_set = lock->set; struct stat st1, st2; const char *error; int ret; temp_set.close_on_free = FALSE; temp_set.unlink_on_free = FALSE; file_unlock_real(lock); ret = file_try_lock(lock->fd, lock->path, F_WRLCK, &temp_set, &temp_lock, &error); if (ret < 0) { i_error("file_lock_free(): Unexpectedly failed to retry locking %s: %s", lock->path, error); } else if (ret == 0) { /* already locked by someone else */ } else if (fstat(lock->fd, &st1) < 0) { /* not expected to happen */ i_error("file_lock_free(): fstat(%s) failed: %m", lock->path); } else if (stat(lock->path, &st2) < 0) { if (errno != ENOENT) i_error("file_lock_free(): stat(%s) failed: %m", lock->path); } else if (st1.st_ino != st2.st_ino || !CMP_DEV_T(st1.st_dev, st2.st_dev)) { /* lock file was recreated already - don't delete it */ } else { /* nobody was waiting on the lock - unlink it */ i_unlink(lock->path); } file_lock_free(&temp_lock); } void file_lock_free(struct file_lock **_lock) { struct file_lock *lock = *_lock; if (lock == NULL) return; *_lock = NULL; if (lock->dotlock != NULL) file_dotlock_delete(&lock->dotlock); if (lock->set.unlink_on_free) file_try_unlink_locked(lock); if (lock->set.close_on_free) i_close_fd(&lock->fd); file_lock_log_warning_if_slow(lock); i_free(lock->path); i_free(lock); } const char *file_lock_get_path(struct file_lock *lock) { return lock->path; } void file_lock_set_path(struct file_lock *lock, const char *path) { if (path != lock->path) { i_free(lock->path); lock->path = i_strdup(path); } } void file_lock_wait_start(void) { i_assert(lock_wait_start.tv_sec == 0); i_gettimeofday(&lock_wait_start); } static void file_lock_wait_init_warning(void) { const char *value; i_assert(file_lock_slow_warning_usecs == -1); value = getenv("FILE_LOCK_SLOW_WARNING_MSECS"); if (value == NULL) file_lock_slow_warning_usecs = LLONG_MAX; else if (str_to_llong(value, &file_lock_slow_warning_usecs) == 0 && file_lock_slow_warning_usecs > 0) { file_lock_slow_warning_usecs *= 1000; } else { i_error("FILE_LOCK_SLOW_WARNING_MSECS: " "Invalid value '%s' - ignoring", value); file_lock_slow_warning_usecs = LLONG_MAX; } } static void file_lock_log_warning_if_slow(struct file_lock *lock) { if (file_lock_slow_warning_usecs < 0) file_lock_wait_init_warning(); if (file_lock_slow_warning_usecs == LLONG_MAX) { /* slowness checking is disabled */ return; } if (lock->lock_type != F_WRLCK) { /* some shared locks can legitimately be kept for a long time. don't warn about them. */ return; } struct timeval now; i_gettimeofday(&now); int diff = timeval_diff_msecs(&now, &lock->locked_time); if (diff > file_lock_slow_warning_usecs/1000) { i_warning("Lock %s kept for %d.%03d secs", lock->path, diff / 1000, diff % 1000); } } void file_lock_wait_end(const char *lock_name) { struct timeval now; i_assert(lock_wait_start.tv_sec != 0); i_gettimeofday(&now); long long diff = timeval_diff_usecs(&now, &lock_wait_start); if (diff < 0) { /* time moved backwards */ diff = 0; } if (diff > file_lock_slow_warning_usecs) { if (file_lock_slow_warning_usecs < 0) file_lock_wait_init_warning(); if (diff > file_lock_slow_warning_usecs) { int diff_msecs = (diff + 999) / 1000; i_warning("Locking %s took %d.%03d secs", lock_name, diff_msecs / 1000, diff_msecs % 1000); } } file_lock_wait_usecs += diff; lock_wait_start.tv_sec = 0; } uint64_t file_lock_wait_get_total_usecs(void) { return file_lock_wait_usecs; }