summaryrefslogtreecommitdiffstats
path: root/src/exim_lock.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/exim_lock.c663
1 files changed, 663 insertions, 0 deletions
diff --git a/src/exim_lock.c b/src/exim_lock.c
new file mode 100644
index 0000000..427d22c
--- /dev/null
+++ b/src/exim_lock.c
@@ -0,0 +1,663 @@
+/* A program to lock a file exactly as Exim would, for investigation of
+interlocking problems.
+
+Options: -fcntl use fcntl() lock
+ -flock use flock() lock
+ -lockfile use lock file
+ -mbx use mbx locking rules, with either fcntl() or flock()
+
+Default is -fcntl -lockfile.
+
+Argument: the name of the lock file
+
+Copyright (c) The Exim Maintainers 2016 - 2021
+*/
+
+#include "os.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <signal.h>
+#include <errno.h>
+#include <time.h>
+#include <netdb.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <utime.h>
+#include <sys/utsname.h>
+#include <sys/stat.h>
+#include <sys/file.h>
+#include <pwd.h>
+
+/* Not all systems have flock() available. Those that do must define LOCK_SH
+in sys/file.h. */
+
+#ifndef LOCK_SH
+#define NO_FLOCK
+#endif
+
+
+typedef unsigned BOOL;
+#define FALSE 0
+#define TRUE 1
+
+
+/* Flag for timeout signal handler */
+
+static int sigalrm_seen = FALSE;
+
+
+/* We need to pull in strerror() and os_non_restarting_signal() from the
+os.c source, if they are required for this OS. However, we don't need any of
+the other stuff in os.c, so force the other macros to omit it. */
+
+#ifndef OS_RESTARTING_SIGNAL
+ #define OS_RESTARTING_SIGNAL
+#endif
+
+#ifndef OS_STRSIGNAL
+ #define OS_STRSIGNAL
+#endif
+
+#ifndef OS_STREXIT
+ #define OS_STREXIT
+#endif
+
+#ifndef OS_LOAD_AVERAGE
+ #define OS_LOAD_AVERAGE
+#endif
+
+#ifndef FIND_RUNNING_INTERFACES
+ #define FIND_RUNNING_INTERFACES
+#endif
+
+#ifndef OS_GET_DNS_RESOLVER_RES
+ #define OS_GET_DNS_RESOLVER_RES
+#endif
+
+#include "../src/os.c"
+
+
+
+/*************************************************
+* Timeout handler *
+*************************************************/
+
+static void
+sigalrm_handler(int sig)
+{
+sigalrm_seen = TRUE;
+}
+
+
+
+/*************************************************
+* Give usage and die *
+*************************************************/
+
+static void
+usage(void)
+{
+printf("usage: exim_lock [-v] [-q] [-lockfile] [-fcntl] [-flock] [-mbx]\n"
+ " [-retries <n>] [-interval <n>] [-timeout <n>] [-restore-times]\n"
+ " <file name> [command]\n");
+exit(1);
+}
+
+
+
+/*************************************************
+* Apply a lock to a file descriptor *
+*************************************************/
+
+static int
+apply_lock(int fd, int fcntltype, BOOL dofcntl, int fcntltime, BOOL doflock,
+ int flocktime)
+{
+int yield = 0;
+int save_errno;
+struct flock lock_data;
+lock_data.l_type = fcntltype;
+lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
+
+sigalrm_seen = FALSE;
+
+if (dofcntl)
+ {
+ if (fcntltime > 0)
+ {
+ os_non_restarting_signal(SIGALRM, sigalrm_handler);
+ alarm(fcntltime);
+ yield = fcntl(fd, F_SETLKW, &lock_data);
+ save_errno = errno;
+ alarm(0);
+ errno = save_errno;
+ }
+ else yield = fcntl(fd, F_SETLK, &lock_data);
+ if (yield < 0) printf("exim_lock: fcntl() failed: %s\n", strerror(errno));
+ }
+
+#ifndef NO_FLOCK
+if (doflock && (yield >= 0))
+ {
+ int flocktype = (fcntltype == F_WRLCK)? LOCK_EX : LOCK_SH;
+ if (flocktime > 0)
+ {
+ os_non_restarting_signal(SIGALRM, sigalrm_handler);
+ alarm(flocktime);
+ yield = flock(fd, flocktype);
+ save_errno = errno;
+ alarm(0);
+ errno = save_errno;
+ }
+ else yield = flock(fd, flocktype | LOCK_NB);
+ if (yield < 0) printf("exim_lock: flock() failed: %s\n", strerror(errno));
+ }
+#endif
+
+return yield;
+}
+
+
+
+/*************************************************
+* The exim_lock program *
+*************************************************/
+
+int main(int argc, char **argv)
+{
+int lock_retries = 10;
+int lock_interval = 3;
+int lock_fcntl_timeout = 0;
+int lock_flock_timeout = 0;
+int i, j, len;
+int fd = -1;
+int hd = -1;
+int md = -1;
+int yield = 0;
+time_t now = time(NULL);
+BOOL use_lockfile = FALSE;
+BOOL use_fcntl = FALSE;
+BOOL use_flock = FALSE;
+BOOL use_mbx = FALSE;
+BOOL verbose = FALSE;
+BOOL quiet = FALSE;
+BOOL restore_times = FALSE;
+char *filename;
+char *lockname = NULL, *hitchname = NULL;
+char *primary_hostname;
+const char *command;
+struct utsname s;
+char buffer[256];
+char tempname[256];
+
+/* Decode options */
+
+for (i = 1; i < argc; i++)
+ {
+ char *arg = argv[i];
+ if (*arg != '-') break;
+ if (strcmp(arg, "-fcntl") == 0) use_fcntl = TRUE;
+ else if (strcmp(arg, "-flock") == 0) use_flock = TRUE;
+ else if (strcmp(arg, "-lockfile") == 0) use_lockfile = TRUE;
+ else if (strcmp(arg, "-mbx") == 0) use_mbx = TRUE;
+ else if (strcmp(arg, "-v") == 0) verbose = TRUE;
+ else if (strcmp(arg, "-q") == 0) quiet = TRUE;
+ else if (strcmp(arg, "-restore-times") == 0) restore_times = TRUE;
+ else if (++i < argc)
+ {
+ int value = atoi(argv[i]);
+ if (strcmp(arg, "-retries") == 0) lock_retries = value;
+ else if (strcmp(arg, "-interval") == 0) lock_interval = value;
+ else if (strcmp(arg, "-timeout") == 0)
+ lock_fcntl_timeout = lock_flock_timeout = value;
+ else usage();
+ }
+ else usage();
+ }
+
+if (quiet) verbose = FALSE;
+
+/* Can't use flock() if the OS doesn't provide it */
+
+#ifdef NO_FLOCK
+if (use_flock)
+ {
+ printf("exim_lock: can't use flock() because it was not available in the\n"
+ " operating system when exim_lock was compiled\n");
+ exit(1);
+ }
+#endif
+
+/* Default is to use lockfiles and fcntl(). */
+
+if (!use_lockfile && !use_fcntl && !use_flock && !use_mbx)
+ use_lockfile = use_fcntl = TRUE;
+
+/* Default fcntl() for use with mbx */
+
+if (use_mbx && !use_fcntl && !use_flock) use_fcntl = TRUE;
+
+/* Unset unused timeouts */
+
+if (!use_fcntl) lock_fcntl_timeout = 0;
+if (!use_flock) lock_flock_timeout = 0;
+
+/* A file name is required */
+
+if (i >= argc) usage();
+
+filename = argv[i++];
+
+/* Expand file names starting with ~ */
+
+if (*filename == '~')
+ {
+ struct passwd *pw;
+
+ if (*(++filename) == '/')
+ pw = getpwuid(getuid());
+ else
+ {
+ char *s = buffer;
+ while (*filename != 0 && *filename != '/')
+ *s++ = *filename++;
+ *s = 0;
+ pw = getpwnam(buffer);
+ }
+
+ if (pw == NULL)
+ {
+ printf("exim_lock: unable to expand file name %s\n", argv[i-1]);
+ exit(1);
+ }
+
+ if ((int)strlen(pw->pw_dir) + (int)strlen(filename) + 1 > sizeof(buffer))
+ {
+ printf("exim_lock: expanded file name %s%s is too long", pw->pw_dir,
+ filename);
+ exit(1);
+ }
+
+ strcpy(buffer, pw->pw_dir);
+ strcat(buffer, filename);
+ filename = buffer;
+ }
+
+/* If using a lock file, prepare by creating the lock file name and
+the hitching post name. */
+
+if (use_lockfile)
+ {
+ if (uname(&s) < 0)
+ {
+ printf("exim_lock: failed to find host name using uname()\n");
+ exit(1);
+ }
+ primary_hostname = s.nodename;
+
+ len = (int)strlen(filename);
+ lockname = malloc(len + 8);
+ sprintf(lockname, "%s.lock", filename);
+ hitchname = malloc(len + 32 + (int)strlen(primary_hostname));
+
+ /* Presumably, this must match appendfile.c */
+ sprintf(hitchname, "%s.%s.%08x.%08x", lockname, primary_hostname,
+ (unsigned int)now, (unsigned int)getpid());
+
+ if (verbose)
+ printf("exim_lock: lockname = %s\n hitchname = %s\n", lockname,
+ hitchname);
+ }
+
+/* Locking retry loop */
+
+for (j = 0; j < lock_retries; j++)
+ {
+ int sleep_before_retry = TRUE;
+ struct stat statbuf, ostatbuf, lstatbuf, statbuf2;
+ int mbx_tmp_oflags;
+
+ /* Try to build a lock file if so configured */
+
+ if (use_lockfile)
+ {
+ int rc, rc2;
+ if (verbose) printf("exim_lock: creating lock file\n");
+ hd = open(hitchname, O_WRONLY | O_CREAT | O_EXCL, 0440);
+ if (hd < 0)
+ {
+ printf("exim_lock: failed to create hitching post %s: %s\n", hitchname,
+ strerror(errno));
+ exit(1);
+ }
+
+ /* Apply hitching post algorithm. */
+
+ if ((rc = link(hitchname, lockname)) != 0)
+ rc2 = fstat(hd, &statbuf);
+ (void)close(hd);
+ unlink(hitchname);
+
+ if (rc != 0 && (rc2 != 0 || statbuf.st_nlink != 2))
+ {
+ printf("exim_lock: failed to link hitching post to lock file\n");
+ hd = -1;
+ goto RETRY;
+ }
+
+ if (!quiet) printf("exim_lock: lock file successfully created\n");
+ }
+
+ /* We are done if no other locking required. */
+
+ if (!use_fcntl && !use_flock && !use_mbx) break;
+
+ /* Open the file for writing. */
+
+ if ((fd = open(filename, O_RDWR + O_APPEND)) < 0)
+ {
+ printf("exim_lock: failed to open %s for writing: %s\n", filename,
+ strerror(errno));
+ yield = 1;
+ goto CLEAN_UP;
+ }
+
+ /* If there is a timeout, implying blocked locking, we don't want to
+ sleep before any retries after this. */
+
+ if (lock_fcntl_timeout > 0 || lock_flock_timeout > 0)
+ sleep_before_retry = FALSE;
+
+ /* Lock using fcntl. There are pros and cons to using a blocking call vs
+ a non-blocking call and retries. Exim is non-blocking by default, but setting
+ a timeout changes it to blocking. */
+
+ if (!use_mbx && (use_fcntl || use_flock))
+ if (apply_lock(fd, F_WRLCK, use_fcntl, lock_fcntl_timeout, use_flock,
+ lock_flock_timeout) >= 0)
+ {
+ if (!quiet)
+ {
+ if (use_fcntl) printf("exim_lock: fcntl() lock successfully applied\n");
+ if (use_flock) printf("exim_lock: flock() lock successfully applied\n");
+ }
+ break;
+ }
+ else
+ goto RETRY; /* Message already output */
+
+ /* Lock using MBX rules. This is complicated and is documented with the
+ source of the c-client library that goes with Pine and IMAP. What has to
+ be done to interwork correctly is to take out a shared lock on the mailbox,
+ and an exclusive lock on a /tmp file. */
+
+ else
+ {
+ if (apply_lock(fd, F_RDLCK, use_fcntl, lock_fcntl_timeout, use_flock,
+ lock_flock_timeout) >= 0)
+ {
+ if (!quiet)
+ {
+ if (use_fcntl)
+ printf("exim_lock: fcntl() read lock successfully applied\n");
+ if (use_flock)
+ printf("exim_lock: fcntl() read lock successfully applied\n");
+ }
+ }
+ else goto RETRY; /* Message already output */
+
+ if (fstat(fd, &statbuf) < 0)
+ {
+ printf("exim_lock: fstat() of %s failed: %s\n", filename,
+ strerror(errno));
+ yield = 1;
+ goto CLEAN_UP;
+ }
+
+ /* Set up file in /tmp and check its state if already existing. */
+
+ sprintf(tempname, "/tmp/.%lx.%lx", (long)statbuf.st_dev,
+ (long)statbuf.st_ino);
+
+ if (lstat(tempname, &statbuf) >= 0)
+ {
+ if ((statbuf.st_mode & S_IFMT) == S_IFLNK)
+ {
+ printf("exim_lock: symbolic link on lock name %s\n", tempname);
+ yield = 1;
+ goto CLEAN_UP;
+ }
+ if (statbuf.st_nlink > 1)
+ {
+ printf("exim_lock: hard link to lock name %s\n", tempname);
+ yield = 1;
+ goto CLEAN_UP;
+ }
+ }
+
+ mbx_tmp_oflags = O_RDWR | O_CREAT;
+#ifdef O_NOFOLLOW
+ mbx_tmp_oflags |= O_NOFOLLOW;
+#endif
+ md = open(tempname, mbx_tmp_oflags, 0600);
+ if (md < 0)
+ {
+ printf("exim_lock: failed to create mbx lock file %s: %s\n",
+ tempname, strerror(errno));
+ goto CLEAN_UP;
+ }
+
+ /* security fixes from 2010-05 */
+ if (lstat(tempname, &lstatbuf) < 0)
+ {
+ printf("exim_lock: failed to lstat(%s) after opening it: %s\n",
+ tempname, strerror(errno));
+ goto CLEAN_UP;
+ }
+ if (fstat(md, &statbuf2) < 0)
+ {
+ printf("exim_lock: failed to fstat() open fd of \"%s\": %s\n",
+ tempname, strerror(errno));
+ goto CLEAN_UP;
+ }
+ if ((statbuf2.st_nlink > 1) ||
+ (lstatbuf.st_nlink > 1) ||
+ (!S_ISREG(lstatbuf.st_mode)) ||
+ (lstatbuf.st_dev != statbuf2.st_dev) ||
+ (lstatbuf.st_ino != statbuf2.st_ino))
+ {
+ printf("exim_lock: race condition exploited against us when "
+ "locking \"%s\"\n", tempname);
+ goto CLEAN_UP;
+ }
+
+ (void)chmod(tempname, 0600);
+
+ if (apply_lock(md, F_WRLCK, use_fcntl, lock_fcntl_timeout, use_flock,
+ lock_flock_timeout) >= 0)
+ {
+ if (!quiet)
+ {
+ if (use_fcntl)
+ printf("exim_lock: fcntl() lock successfully applied to mbx "
+ "lock file %s\n", tempname);
+ if (use_flock)
+ printf("exim_lock: flock() lock successfully applied to mbx "
+ "lock file %s\n", tempname);
+ }
+
+ /* This test checks for a race condition */
+
+ if (lstat(tempname, &statbuf) != 0 ||
+ fstat(md, &ostatbuf) != 0 ||
+ statbuf.st_dev != ostatbuf.st_dev ||
+ statbuf.st_ino != ostatbuf.st_ino)
+ {
+ if (!quiet) printf("exim_lock: mbx lock file %s changed between "
+ "creation and locking\n", tempname);
+ goto RETRY;
+ }
+ else break;
+ }
+ else goto RETRY; /* Message already output */
+ }
+
+ /* Clean up before retrying */
+
+ RETRY:
+
+ if (md >= 0)
+ {
+ if (close(md) < 0)
+ printf("exim_lock: close %s failed: %s\n", tempname, strerror(errno));
+ else
+ if (!quiet) printf("exim_lock: %s closed\n", tempname);
+ md = -1;
+ }
+
+ if (fd >= 0)
+ {
+ if (close(fd) < 0)
+ printf("exim_lock: close failed: %s\n", strerror(errno));
+ else
+ if (!quiet) printf("exim_lock: file closed\n");
+ fd = -1;
+ }
+
+ if (hd >= 0)
+ {
+ if (unlink(lockname) < 0)
+ printf("exim_lock: unlink of %s failed: %s\n", lockname, strerror(errno));
+ else
+ if (!quiet) printf("exim_lock: lock file removed\n");
+ hd = -1;
+ }
+
+ /* If a blocking call timed out, break the retry loop if the total time
+ so far is not less than than retries * interval. */
+
+ if (sigalrm_seen &&
+ (j + 1) * ((lock_fcntl_timeout > lock_flock_timeout)?
+ lock_fcntl_timeout : lock_flock_timeout) >=
+ lock_retries * lock_interval)
+ j = lock_retries;
+
+ /* Wait a bit before retrying, except when it was a blocked fcntl() that
+ caused the problem. */
+
+ if (j < lock_retries && sleep_before_retry)
+ {
+ printf(" ... waiting\n");
+ sleep(lock_interval);
+ }
+ }
+
+if (j >= lock_retries)
+ {
+ printf("exim_lock: locking failed too many times\n");
+ yield = 1;
+ goto CLEAN_UP;
+ }
+
+if (!quiet) printf("exim_lock: locking %s succeeded: ", filename);
+
+/* If there are no further arguments, run the user's shell; otherwise
+the next argument is a command to run. */
+
+if (i >= argc)
+ {
+ command = getenv("SHELL");
+ if (command == NULL || *command == 0) command = "/bin/sh";
+ if (!quiet) printf("running %s ...\n", command);
+ }
+else
+ {
+ command = argv[i];
+ if (!quiet) printf("running the command ...\n");
+ }
+
+/* Run the command, saving and restoring the times if required. */
+
+if (restore_times)
+ {
+ struct stat strestore;
+#ifdef EXIM_HAVE_FUTIMENS
+ int fd = open(filename, O_RDWR); /* use fd for both get & restore */
+ struct timespec tt[2];
+
+ if (fd < 0)
+ {
+ printf("open '%s': %s\n", filename, strerror(errno));
+ yield = 1;
+ goto CLEAN_UP;
+ }
+ if (fstat(fd, &strestore) != 0)
+ {
+ printf("fstat '%s': %s\n", filename, strerror(errno));
+ yield = 1;
+ close(fd);
+ goto CLEAN_UP;
+ }
+ i = system(command);
+ tt[0] = strestore.st_atim;
+ tt[1] = strestore.st_mtim;
+ (void) futimens(fd, tt);
+ (void) close(fd);
+#else
+ struct utimbuf ut;
+
+ stat(filename, &strestore);
+ i = system(command);
+ ut.actime = strestore.st_atime;
+ ut.modtime = strestore.st_mtime;
+ utime(filename, &ut);
+#endif
+ }
+else i = system(command);
+
+if(i && !quiet) printf("warning: nonzero status %d\n", i);
+
+/* Remove the locks and exit. Unlink the /tmp file if we can get an exclusive
+lock on the mailbox. This should be a non-blocking lock call, as there is no
+point in waiting. */
+
+CLEAN_UP:
+
+if (md >= 0)
+ {
+ if (apply_lock(fd, F_WRLCK, use_fcntl, 0, use_flock, 0) >= 0)
+ {
+ if (!quiet) printf("exim_lock: %s unlinked - no sharers\n", tempname);
+ unlink(tempname);
+ }
+ else if (!quiet)
+ printf("exim_lock: %s not unlinked - unable to get exclusive mailbox lock\n",
+ tempname);
+ if (close(md) < 0)
+ printf("exim_lock: close %s failed: %s\n", tempname, strerror(errno));
+ else
+ if (!quiet) printf("exim_lock: %s closed\n", tempname);
+ }
+
+if (fd >= 0)
+ {
+ if (close(fd) < 0)
+ printf("exim_lock: close %s failed: %s\n", filename, strerror(errno));
+ else
+ if (!quiet) printf("exim_lock: %s closed\n", filename);
+ }
+
+if (hd >= 0)
+ {
+ if (unlink(lockname) < 0)
+ printf("exim_lock: unlink %s failed: %s\n", lockname, strerror(errno));
+ else
+ if (!quiet) printf("exim_lock: lock file removed\n");
+ }
+
+return yield;
+}
+
+/* End */