diff options
Diffstat (limited to '')
-rw-r--r-- | libmount/src/lock.c | 721 |
1 files changed, 721 insertions, 0 deletions
diff --git a/libmount/src/lock.c b/libmount/src/lock.c new file mode 100644 index 0000000..e6eefa1 --- /dev/null +++ b/libmount/src/lock.c @@ -0,0 +1,721 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * This file is part of libmount from util-linux project. + * + * Copyright (C) 2009-2018 Karel Zak <kzak@redhat.com> + * + * libmount is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + */ + +/** + * SECTION: lock + * @title: Locking + * @short_description: locking methods for /etc/mtab or another libmount files + * + * The mtab lock is backwards compatible with the standard linux /etc/mtab + * locking. Note, it's necessary to use the same locking schema in all + * applications that access the file. + */ +#include <sys/time.h> +#include <time.h> +#include <signal.h> +#include <fcntl.h> +#include <limits.h> +#include <sys/file.h> + +#include "strutils.h" +#include "closestream.h" +#include "pathnames.h" +#include "mountP.h" +#include "monotonic.h" + +/* + * lock handler + */ +struct libmnt_lock { + char *lockfile; /* path to lock file (e.g. /etc/mtab~) */ + char *linkfile; /* path to link file (e.g. /etc/mtab~.<id>) */ + int lockfile_fd; /* lock file descriptor */ + + unsigned int locked :1, /* do we own the lock? */ + sigblock :1, /* block signals when locked */ + simplelock :1; /* use flock rather than normal mtab lock */ + + sigset_t oldsigmask; +}; + + +/** + * mnt_new_lock: + * @datafile: the file that should be covered by the lock + * @id: unique linkfile identifier or 0 (default is getpid()) + * + * Returns: newly allocated lock handler or NULL on case of error. + */ +struct libmnt_lock *mnt_new_lock(const char *datafile, pid_t id) +{ + struct libmnt_lock *ml = NULL; + char *lo = NULL, *ln = NULL; + size_t losz; + + if (!datafile) + return NULL; + + /* for flock we use "foo.lock, for mtab "foo~" + */ + losz = strlen(datafile) + sizeof(".lock"); + lo = malloc(losz); + if (!lo) + goto err; + + /* default is mtab~ lock */ + snprintf(lo, losz, "%s~", datafile); + + if (asprintf(&ln, "%s~.%d", datafile, id ? : getpid()) == -1) { + ln = NULL; + goto err; + } + ml = calloc(1, sizeof(*ml) ); + if (!ml) + goto err; + + ml->lockfile_fd = -1; + ml->linkfile = ln; + ml->lockfile = lo; + + DBG(LOCKS, ul_debugobj(ml, "alloc: default linkfile=%s, lockfile=%s", ln, lo)); + return ml; +err: + free(lo); + free(ln); + free(ml); + return NULL; +} + + +/** + * mnt_free_lock: + * @ml: struct libmnt_lock handler + * + * Deallocates mnt_lock. + */ +void mnt_free_lock(struct libmnt_lock *ml) +{ + if (!ml) + return; + DBG(LOCKS, ul_debugobj(ml, "free%s", ml->locked ? " !!! LOCKED !!!" : "")); + free(ml->lockfile); + free(ml->linkfile); + free(ml); +} + +/** + * mnt_lock_block_signals: + * @ml: struct libmnt_lock handler + * @enable: TRUE/FALSE + * + * Block/unblock signals when the lock is locked, the signals are not blocked + * by default. + * + * Returns: <0 on error, 0 on success. + */ +int mnt_lock_block_signals(struct libmnt_lock *ml, int enable) +{ + if (!ml) + return -EINVAL; + DBG(LOCKS, ul_debugobj(ml, "signals: %s", enable ? "BLOCKED" : "UNBLOCKED")); + ml->sigblock = enable ? 1 : 0; + return 0; +} + +/* don't export this to API + */ +int mnt_lock_use_simplelock(struct libmnt_lock *ml, int enable) +{ + size_t sz; + + if (!ml) + return -EINVAL; + + assert(ml->lockfile); + + DBG(LOCKS, ul_debugobj(ml, "flock: %s", enable ? "ENABLED" : "DISABLED")); + ml->simplelock = enable ? 1 : 0; + + sz = strlen(ml->lockfile); + assert(sz); + + if (sz < 1) + return -EINVAL; + + /* Change lock name: + * + * flock: "<name>.lock" + * mtab lock: "<name>~" + */ + if (ml->simplelock && endswith(ml->lockfile, "~")) + memcpy(ml->lockfile + sz - 1, ".lock", 6); + + else if (!ml->simplelock && endswith(ml->lockfile, ".lock")) + memcpy(ml->lockfile + sz - 5, "~", 2); + + DBG(LOCKS, ul_debugobj(ml, "new lock filename: '%s'", ml->lockfile)); + return 0; +} + +/* + * Returns path to lockfile. + */ +static const char *mnt_lock_get_lockfile(struct libmnt_lock *ml) +{ + return ml ? ml->lockfile : NULL; +} + +/* + * Note that the filename is generated by mnt_new_lock() and depends on + * getpid() or 'id' argument of the mnt_new_lock() function. + * + * Returns: unique (per process/thread) path to linkfile. + */ +static const char *mnt_lock_get_linkfile(struct libmnt_lock *ml) +{ + return ml ? ml->linkfile : NULL; +} + +/* + * Simple flocking + */ +static void unlock_simplelock(struct libmnt_lock *ml) +{ + assert(ml); + assert(ml->simplelock); + + if (ml->lockfile_fd >= 0) { + DBG(LOCKS, ul_debugobj(ml, "%s: unflocking", + mnt_lock_get_lockfile(ml))); + close(ml->lockfile_fd); + } +} + +static int lock_simplelock(struct libmnt_lock *ml) +{ + const char *lfile; + int rc; + + assert(ml); + assert(ml->simplelock); + + lfile = mnt_lock_get_lockfile(ml); + + DBG(LOCKS, ul_debugobj(ml, "%s: locking", lfile)); + + if (ml->sigblock) { + sigset_t sigs; + sigemptyset(&ml->oldsigmask); + sigfillset(&sigs); + sigprocmask(SIG_BLOCK, &sigs, &ml->oldsigmask); + } + + ml->lockfile_fd = open(lfile, O_RDONLY|O_CREAT|O_CLOEXEC, + S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH); + if (ml->lockfile_fd < 0) { + rc = -errno; + goto err; + } + rc = fchmod(ml->lockfile_fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); + if (rc < 0) { + rc = -errno; + goto err; + } + + while (flock(ml->lockfile_fd, LOCK_EX) < 0) { + int errsv; + if ((errno == EAGAIN) || (errno == EINTR)) + continue; + errsv = errno; + close(ml->lockfile_fd); + ml->lockfile_fd = -1; + rc = -errsv; + goto err; + } + ml->locked = 1; + return 0; +err: + if (ml->sigblock) + sigprocmask(SIG_SETMASK, &ml->oldsigmask, NULL); + return rc; +} + +/* + * traditional mtab locking + */ + +static void mnt_lockalrm_handler(int sig __attribute__((__unused__))) +{ + /* do nothing, say nothing, be nothing */ +} + +/* + * Waits for F_SETLKW, unfortunately we have to use SIGALRM here to interrupt + * fcntl() to avoid neverending waiting. + * + * Returns: 0 on success, 1 on timeout, -errno on error. + */ +static int mnt_wait_mtab_lock(struct libmnt_lock *ml, struct flock *fl, time_t maxtime) +{ + struct timeval now; + struct sigaction sa, osa; + int ret = 0; + + gettime_monotonic(&now); + DBG(LOCKS, ul_debugobj(ml, "(%d) waiting for F_SETLKW (now=%lu, maxtime=%lu, diff=%lu)", + getpid(), + (unsigned long) now.tv_sec, + (unsigned long) maxtime, + (unsigned long) (maxtime - now.tv_sec))); + + if (now.tv_sec >= maxtime) + return 1; /* timeout */ + + /* setup ALARM handler -- we don't want to wait forever */ + sa.sa_flags = 0; + sa.sa_handler = mnt_lockalrm_handler; + sigfillset (&sa.sa_mask); + + sigaction(SIGALRM, &sa, &osa); + + + alarm(maxtime - now.tv_sec); + if (fcntl(ml->lockfile_fd, F_SETLKW, fl) == -1) + ret = errno == EINTR ? 1 : -errno; + alarm(0); + + /* restore old sigaction */ + sigaction(SIGALRM, &osa, NULL); + + DBG(LOCKS, ul_debugobj(ml, "(%d) leaving mnt_wait_setlkw(), rc=%d", + getpid(), ret)); + return ret; +} + +/* + * Create the mtab lock file. + * + * The old code here used flock on a lock file /etc/mtab~ and deleted + * this lock file afterwards. However, as rgooch remarks, that has a + * race: a second mount may be waiting on the lock and proceed as + * soon as the lock file is deleted by the first mount, and immediately + * afterwards a third mount comes, creates a new /etc/mtab~, applies + * flock to that, and also proceeds, so that the second and third mount + * are now both scribbling in /etc/mtab. + * + * The new code uses a link() instead of a creat(), where we proceed + * only if it was us that created the lock, and hence we always have + * to delete the lock afterwards. Now the use of flock() is in principle + * superfluous, but avoids an arbitrary sleep(). + * + * Where does the link point to? Obvious choices are mtab and mtab~~. + * HJLu points out that the latter leads to races. Right now we use + * mtab~.pid instead. + * + * + * The original mount locking code has used sleep(1) between attempts and + * maximal number of attempts has been 5. + * + * There was a very small number of attempts and extremely long waiting (1s) + * that is useless on machines with large number of mount processes. + * + * Now we wait for a few thousand microseconds between attempts and we have a global + * time limit (30s) rather than a limit for the number of attempts. The advantage + * is that this method also counts time which we spend in fcntl(F_SETLKW) and + * the number of attempts is not restricted. + * -- kzak@redhat.com [Mar-2007] + * + * + * This mtab locking code has been refactored and moved to libmount. The mtab + * locking is really not perfect (e.g. SIGALRM), but it's stable, reliable and + * backwardly compatible code. + * + * Don't forget that this code has to be compatible with 3rd party mounts + * (/sbin/mount.foo) and has to work with NFS. + * -- kzak@redhat.com [May-2009] + */ + +/* maximum seconds between the first and the last attempt */ +#define MOUNTLOCK_MAXTIME 30 + +/* sleep time (in microseconds, max=999999) between attempts */ +#define MOUNTLOCK_WAITTIME 5000 + +static void unlock_mtab(struct libmnt_lock *ml) +{ + if (!ml) + return; + + if (!ml->locked && ml->lockfile && ml->linkfile) + { + /* We (probably) have all the files, but we don't own the lock, + * Really? Check it! Maybe ml->locked wasn't set properly + * because the code was interrupted by a signal. Paranoia? Yes. + * + * We own the lock when linkfile == lockfile. + */ + struct stat lo, li; + + if (!stat(ml->lockfile, &lo) && !stat(ml->linkfile, &li) && + lo.st_dev == li.st_dev && lo.st_ino == li.st_ino) + ml->locked = 1; + } + + if (ml->linkfile) + unlink(ml->linkfile); + if (ml->lockfile_fd >= 0) + close(ml->lockfile_fd); + if (ml->locked && ml->lockfile) { + unlink(ml->lockfile); + DBG(LOCKS, ul_debugobj(ml, "unlink %s", ml->lockfile)); + } +} + +static int lock_mtab(struct libmnt_lock *ml) +{ + int i, rc = -1; + struct timespec waittime; + struct timeval maxtime; + const char *lockfile, *linkfile; + + if (!ml) + return -EINVAL; + if (ml->locked) + return 0; + + lockfile = mnt_lock_get_lockfile(ml); + if (!lockfile) + return -EINVAL; + linkfile = mnt_lock_get_linkfile(ml); + if (!linkfile) + return -EINVAL; + + if (ml->sigblock) { + /* + * Block all signals when locked, mnt_unlock_file() will + * restore the old mask. + */ + sigset_t sigs; + + sigemptyset(&ml->oldsigmask); + sigfillset(&sigs); + sigdelset(&sigs, SIGTRAP); + sigdelset(&sigs, SIGALRM); + sigprocmask(SIG_BLOCK, &sigs, &ml->oldsigmask); + } + + i = open(linkfile, O_WRONLY|O_CREAT|O_CLOEXEC, S_IRUSR|S_IWUSR); + if (i < 0) { + /* linkfile does not exist (as a file) and we cannot create it. + * Read-only or full filesystem? Too many files open in the system? + */ + if (errno > 0) + rc = -errno; + goto failed; + } + close(i); + + gettime_monotonic(&maxtime); + maxtime.tv_sec += MOUNTLOCK_MAXTIME; + + waittime.tv_sec = 0; + waittime.tv_nsec = (1000 * MOUNTLOCK_WAITTIME); + + /* Repeat until it was us who made the link */ + while (!ml->locked) { + struct timeval now; + struct flock flock; + int j; + + j = link(linkfile, lockfile); + if (j == 0) + ml->locked = 1; + + if (j < 0 && errno != EEXIST) { + if (errno > 0) + rc = -errno; + goto failed; + } + ml->lockfile_fd = open(lockfile, O_WRONLY|O_CLOEXEC); + + if (ml->lockfile_fd < 0) { + /* Strange... Maybe the file was just deleted? */ + int errsv = errno; + gettime_monotonic(&now); + if (errsv == ENOENT && now.tv_sec < maxtime.tv_sec) { + ml->locked = 0; + continue; + } + if (errsv > 0) + rc = -errsv; + goto failed; + } + + flock.l_type = F_WRLCK; + flock.l_whence = SEEK_SET; + flock.l_start = 0; + flock.l_len = 0; + + if (ml->locked) { + /* We made the link. Now claim the lock. */ + if (fcntl (ml->lockfile_fd, F_SETLK, &flock) == -1) { + DBG(LOCKS, ul_debugobj(ml, + "%s: can't F_SETLK lockfile, errno=%d\n", + lockfile, errno)); + /* proceed, since it was us who created the lockfile anyway */ + } + break; + } else { + /* Someone else made the link. Wait. */ + int err = mnt_wait_mtab_lock(ml, &flock, maxtime.tv_sec); + + if (err == 1) { + DBG(LOCKS, ul_debugobj(ml, + "%s: can't create link: time out (perhaps " + "there is a stale lock file?)", lockfile)); + rc = -ETIMEDOUT; + goto failed; + + } else if (err < 0) { + rc = err; + goto failed; + } + nanosleep(&waittime, NULL); + close(ml->lockfile_fd); + ml->lockfile_fd = -1; + } + } + DBG(LOCKS, ul_debugobj(ml, "%s: (%d) successfully locked", + lockfile, getpid())); + unlink(linkfile); + return 0; + +failed: + mnt_unlock_file(ml); + return rc; +} + + +/** + * mnt_lock_file + * @ml: pointer to struct libmnt_lock instance + * + * Creates a lock file (e.g. /etc/mtab~). Note that this function may + * use alarm(). + * + * Your application always has to call mnt_unlock_file() before exit. + * + * Traditional mtab locking scheme: + * + * 1. create linkfile (e.g. /etc/mtab~.$PID) + * 2. link linkfile --> lockfile (e.g. /etc/mtab~.$PID --> /etc/mtab~) + * 3. a) link() success: setups F_SETLK lock (see fcntl(2)) + * b) link() failed: wait (max 30s) on F_SETLKW lock, goto 2. + * + * Note that when the lock is used by mnt_update_table() interface then libmount + * uses flock() for private library file /run/mount/utab. The fcntl(2) is used only + * for backwardly compatible stuff like /etc/mtab. + * + * Returns: 0 on success or negative number in case of error (-ETIMEOUT is case + * of stale lock file). + */ +int mnt_lock_file(struct libmnt_lock *ml) +{ + if (!ml) + return -EINVAL; + + if (ml->simplelock) + return lock_simplelock(ml); + + return lock_mtab(ml); +} + +/** + * mnt_unlock_file: + * @ml: lock struct + * + * Unlocks the file. The function could be called independently of the + * lock status (for example from exit(3)). + */ +void mnt_unlock_file(struct libmnt_lock *ml) +{ + if (!ml) + return; + + DBG(LOCKS, ul_debugobj(ml, "(%d) %s", getpid(), + ml->locked ? "unlocking" : "cleaning")); + + if (ml->simplelock) + unlock_simplelock(ml); + else + unlock_mtab(ml); + + ml->locked = 0; + ml->lockfile_fd = -1; + + if (ml->sigblock) { + DBG(LOCKS, ul_debugobj(ml, "restoring sigmask")); + sigprocmask(SIG_SETMASK, &ml->oldsigmask, NULL); + } +} + +#ifdef TEST_PROGRAM + +static struct libmnt_lock *lock; + +/* + * read number from @filename, increment the number and + * write the number back to the file + */ +static void increment_data(const char *filename, int verbose, int loopno) +{ + long num; + FILE *f; + char buf[256]; + + if (!(f = fopen(filename, "r" UL_CLOEXECSTR))) + err(EXIT_FAILURE, "%d: failed to open: %s", getpid(), filename); + + if (!fgets(buf, sizeof(buf), f)) + err(EXIT_FAILURE, "%d failed read: %s", getpid(), filename); + + fclose(f); + num = atol(buf) + 1; + + if (!(f = fopen(filename, "w" UL_CLOEXECSTR))) + err(EXIT_FAILURE, "%d: failed to open: %s", getpid(), filename); + + fprintf(f, "%ld", num); + + if (close_stream(f) != 0) + err(EXIT_FAILURE, "write failed: %s", filename); + + if (verbose) + fprintf(stderr, "%d: %s: %ld --> %ld (loop=%d)\n", getpid(), + filename, num - 1, num, loopno); +} + +static void clean_lock(void) +{ + if (!lock) + return; + mnt_unlock_file(lock); + mnt_free_lock(lock); +} + +static void __attribute__((__noreturn__)) sig_handler(int sig) +{ + errx(EXIT_FAILURE, "\n%d: catch signal: %s\n", getpid(), strsignal(sig)); +} + +static int test_lock(struct libmnt_test *ts, int argc, char *argv[]) +{ + time_t synctime = 0; + unsigned int usecs; + const char *datafile = NULL; + int verbose = 0, loops = 0, l, idx = 1; + + if (argc < 3) + return -EINVAL; + + if (strcmp(argv[idx], "--synctime") == 0) { + synctime = (time_t) atol(argv[idx + 1]); + idx += 2; + } + if (idx < argc && strcmp(argv[idx], "--verbose") == 0) { + verbose = 1; + idx++; + } + + if (idx < argc) + datafile = argv[idx++]; + if (idx < argc) + loops = atoi(argv[idx++]); + + if (!datafile || !loops) + return -EINVAL; + + if (verbose) + fprintf(stderr, "%d: start: synctime=%u, datafile=%s, loops=%d\n", + getpid(), (int) synctime, datafile, loops); + + atexit(clean_lock); + + /* be paranoid and call exit() (=clean_lock()) for all signals */ + { + int sig = 0; + struct sigaction sa; + + sa.sa_handler = sig_handler; + sa.sa_flags = 0; + sigfillset(&sa.sa_mask); + + while (sigismember(&sa.sa_mask, ++sig) != -1 && sig != SIGCHLD) + sigaction (sig, &sa, (struct sigaction *) 0); + } + + /* start the test in exactly defined time */ + if (synctime) { + struct timeval tv; + + gettimeofday(&tv, NULL); + if (synctime && synctime - tv.tv_sec > 1) { + usecs = ((synctime - tv.tv_sec) * 1000000UL) - + (1000000UL - tv.tv_usec); + xusleep(usecs); + } + } + + for (l = 0; l < loops; l++) { + lock = mnt_new_lock(datafile, 0); + if (!lock) + return -1; + + if (mnt_lock_file(lock) != 0) { + fprintf(stderr, "%d: failed to lock %s file\n", + getpid(), datafile); + return -1; + } + + increment_data(datafile, verbose, l); + + mnt_unlock_file(lock); + mnt_free_lock(lock); + lock = NULL; + + /* The mount command usually finishes after a mtab update. We + * simulate this via short sleep -- it's also enough to make + * concurrent processes happy. + */ + if (synctime) + xusleep(25000); + } + + return 0; +} + +/* + * Note that this test should be executed from a script that creates many + * parallel processes, otherwise this test does not make sense. + */ +int main(int argc, char *argv[]) +{ + struct libmnt_test tss[] = { + { "--lock", test_lock, " [--synctime <time_t>] [--verbose] <datafile> <loops> " + "increment a number in datafile" }, + { NULL } + }; + + return mnt_run_test(tss, argc, argv); +} + +#endif /* TEST_PROGRAM */ |