Rocco Rutte:
[apps/madmutt.git] / dotlock.c
1 /*
2  * Copyright notice from original mutt:
3  * Copyright (C) 1996-2000 Michael R. Elkins <me@mutt.org>
4  * Copyright (C) 1998-2000 Thomas Roessler <roessler@does-not-exist.org>
5  *
6  * This file is part of mutt-ng, see http://www.muttng.org/.
7  * It's licensed under the GNU General Public License,
8  * please see the file GPL in the top level source directory.
9  */
10
11 /*
12  * This module either be compiled into Mutt, or it can be
13  * built as a separate program. For building it
14  * separately, define the DL_STANDALONE preprocessor
15  * macro.
16  */
17
18 #if HAVE_CONFIG_H
19 # include "config.h"
20 #endif
21
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25
26 #include <unistd.h>
27 #include <dirent.h>
28 #include <sys/file.h>
29 #include <sys/stat.h>
30 #include <sys/utsname.h>
31 #include <errno.h>
32 #include <time.h>
33 #include <fcntl.h>
34 #include <limits.h>
35
36 #ifndef _POSIX_PATH_MAX
37 #include <posix1_lim.h>
38 #endif
39
40 #include "dotlock.h"
41 #include "config.h"
42
43 #ifdef HAVE_GETOPT_H
44 #include <getopt.h>
45 #endif
46
47 #ifdef DL_STANDALONE
48 # include "reldate.h"
49 #endif
50
51 #define MAXLINKS 1024           /* maximum link depth */
52
53 #ifdef DL_STANDALONE
54
55 # define LONG_STRING 1024
56 # define MAXLOCKATTEMPT 5
57
58 # define strfcpy(A,B,C) strncpy (A,B,C), *(A+(C)-1)=0
59
60 # ifdef USE_SETGID
61
62 #  ifdef HAVE_SETEGID
63 #   define SETEGID setegid
64 #  else
65 #   define SETEGID setgid
66 #  endif
67 #  ifndef S_ISLNK
68 #   define S_ISLNK(x) (((x) & S_IFMT) == S_IFLNK ? 1 : 0)
69 #  endif
70
71 # endif
72
73 # ifndef HAVE_SNPRINTF
74 extern int snprintf (char *, size_t, const char *, ...);
75 # endif
76
77 #else /* DL_STANDALONE */
78
79 # ifdef USE_SETGID
80 #   error Do not try to compile dotlock as a mutt module when requiring egid switching!
81 # endif
82
83 # include "mutt.h"
84 # include "mx.h"
85
86 #endif /* DL_STANDALONE */
87
88 static int DotlockFlags;
89 static int Retry = MAXLOCKATTEMPT;
90
91 #ifdef DL_STANDALONE
92 static char *Hostname;
93 #endif
94
95 #ifdef USE_SETGID
96 static gid_t UserGid;
97 static gid_t MailGid;
98 #endif
99
100 static int dotlock_deference_symlink (char *, size_t, const char *);
101 static int dotlock_prepare (char *, size_t, const char *, int fd);
102 static int dotlock_check_stats (struct stat *, struct stat *);
103 static int dotlock_dispatch (const char *, int fd);
104
105 #ifdef DL_STANDALONE
106 static int dotlock_init_privs (void);
107 static void usage (const char *);
108 #endif
109
110 static void dotlock_expand_link (char *, const char *, const char *);
111 static void BEGIN_PRIVILEGED (void);
112 static void END_PRIVILEGED (void);
113
114 /* These functions work on the current directory.
115  * Invoke dotlock_prepare () before and check their
116  * return value.
117  */
118
119 static int dotlock_try (void);
120 static int dotlock_unlock (const char *);
121 static int dotlock_unlink (const char *);
122 static int dotlock_lock (const char *);
123
124
125 #ifdef DL_STANDALONE
126
127 #define check_flags(a) if (a & DL_FL_ACTIONS) usage (argv[0])
128
129 size_t mutt_strlen (const char* s) {
130   return (s ? strlen (s) : 0);
131 }
132
133 int main (int argc, char **argv)
134 {
135   int i;
136   char *p;
137   struct utsname utsname;
138
139   /* first, drop privileges */
140
141   if (dotlock_init_privs () == -1)
142     return DL_EX_ERROR;
143
144
145   /* determine the system's host name */
146
147   uname (&utsname);
148   if (!(Hostname = strdup (utsname.nodename)))  /* __MEM_CHECKED__ */
149     return DL_EX_ERROR;
150   if ((p = strchr (Hostname, '.')))
151     *p = '\0';
152
153
154   /* parse the command line options. */
155   DotlockFlags = 0;
156
157   while ((i = getopt (argc, argv, "dtfupr:")) != EOF) {
158     switch (i) {
159       /* actions, mutually exclusive */
160     case 't':
161       check_flags (DotlockFlags);
162       DotlockFlags |= DL_FL_TRY;
163       break;
164     case 'd':
165       check_flags (DotlockFlags);
166       DotlockFlags |= DL_FL_UNLINK;
167       break;
168     case 'u':
169       check_flags (DotlockFlags);
170       DotlockFlags |= DL_FL_UNLOCK;
171       break;
172
173       /* other flags */
174     case 'f':
175       DotlockFlags |= DL_FL_FORCE;
176       break;
177     case 'p':
178       DotlockFlags |= DL_FL_USEPRIV;
179       break;
180     case 'r':
181       DotlockFlags |= DL_FL_RETRY;
182       Retry = atoi (optarg);
183       break;
184
185     default:
186       usage (argv[0]);
187     }
188   }
189
190   if (optind == argc || Retry < 0)
191     usage (argv[0]);
192
193   return dotlock_dispatch (argv[optind], -1);
194 }
195
196
197 /* 
198  * Determine our effective group ID, and drop 
199  * privileges.
200  * 
201  * Return value:
202  * 
203  *  0 - everything went fine
204  * -1 - we couldn't drop privileges.
205  * 
206  */
207
208
209 static int dotlock_init_privs (void)
210 {
211
212 # ifdef USE_SETGID
213
214   UserGid = getgid ();
215   MailGid = getegid ();
216
217   if (SETEGID (UserGid) != 0)
218     return -1;
219
220 # endif
221
222   return 0;
223 }
224
225
226 #else /* DL_STANDALONE */
227
228 /* 
229  * This function is intended to be invoked from within
230  * mutt instead of mx.c's invoke_dotlock ().
231  */
232
233 int dotlock_invoke (const char *path, int fd, int flags, int retry)
234 {
235   int currdir;
236   int r;
237
238   DotlockFlags = flags;
239
240   if ((currdir = open (".", O_RDONLY)) == -1)
241     return DL_EX_ERROR;
242
243   if (!(DotlockFlags & DL_FL_RETRY) || retry)
244     Retry = MAXLOCKATTEMPT;
245   else
246     Retry = 0;
247
248   r = dotlock_dispatch (path, fd);
249
250   fchdir (currdir);
251   close (currdir);
252
253   return r;
254 }
255
256 #endif /* DL_STANDALONE */
257
258
259 static int dotlock_dispatch (const char *f, int fd)
260 {
261   char realpath[_POSIX_PATH_MAX];
262
263   /* If dotlock_prepare () succeeds [return value == 0],
264    * realpath contains the basename of f, and we have
265    * successfully changed our working directory to
266    * `dirname $f`.  Additionally, f has been opened for
267    * reading to verify that the user has at least read
268    * permissions on that file.
269    * 
270    * For a more detailed explanation of all this, see the
271    * lengthy comment below.
272    */
273
274   if (dotlock_prepare (realpath, sizeof (realpath), f, fd) != 0)
275     return DL_EX_ERROR;
276
277   /* Actually perform the locking operation. */
278
279   if (DotlockFlags & DL_FL_TRY)
280     return dotlock_try ();
281   else if (DotlockFlags & DL_FL_UNLOCK)
282     return dotlock_unlock (realpath);
283   else if (DotlockFlags & DL_FL_UNLINK)
284     return dotlock_unlink (realpath);
285   else                          /* lock */
286     return dotlock_lock (realpath);
287 }
288
289
290 /*
291  * Get privileges 
292  * 
293  * This function re-acquires the privileges we may have
294  * if the user told us to do so by giving the "-p"
295  * command line option.
296  * 
297  * BEGIN_PRIVILEGES () won't return if an error occurs.
298  * 
299  */
300
301 static void BEGIN_PRIVILEGED (void)
302 {
303 #ifdef USE_SETGID
304   if (DotlockFlags & DL_FL_USEPRIV) {
305     if (SETEGID (MailGid) != 0) {
306       /* perror ("setegid"); */
307       exit (DL_EX_ERROR);
308     }
309   }
310 #endif
311 }
312
313 /*
314  * Drop privileges
315  * 
316  * This function drops the group privileges we may have.
317  * 
318  * END_PRIVILEGED () won't return if an error occurs.
319  *
320  */
321
322 static void END_PRIVILEGED (void)
323 {
324 #ifdef USE_SETGID
325   if (DotlockFlags & DL_FL_USEPRIV) {
326     if (SETEGID (UserGid) != 0) {
327       /* perror ("setegid"); */
328       exit (DL_EX_ERROR);
329     }
330   }
331 #endif
332 }
333
334 #ifdef DL_STANDALONE
335
336 /*
337  * Usage information.
338  * 
339  * This function doesn't return.
340  * 
341  */
342
343 static void usage (const char *av0)
344 {
345   fprintf (stderr, "dotlock [Mutt-ng %s (%s)]\n", VERSION, ReleaseDate);
346   fprintf (stderr, "usage: %s [-t|-f|-u|-d] [-p] [-r <retries>] file\n", av0);
347
348   fputs ("\noptions:"
349          "\n  -t\t\ttry"
350          "\n  -f\t\tforce"
351          "\n  -u\t\tunlock" "\n  -d\t\tunlink" "\n  -p\t\tprivileged"
352 #ifndef USE_SETGID
353          " (ignored)"
354 #endif
355          "\n  -r <retries>\tRetry locking" "\n", stderr);
356
357   exit (DL_EX_ERROR);
358 }
359
360 #endif
361
362 /*
363  * Access checking: Let's avoid to lock other users' mail
364  * spool files if we aren't permitted to read them.
365  * 
366  * Some simple-minded access (2) checking isn't sufficient
367  * here: The problem is that the user may give us a
368  * deeply nested path to a file which has the same name
369  * as the file he wants to lock, but different
370  * permissions, say, e.g.
371  * /tmp/lots/of/subdirs/var/spool/mail/root.
372  * 
373  * He may then try to replace /tmp/lots/of/subdirs by a
374  * symbolic link to / after we have invoked access () to
375  * check the file's permissions.  The lockfile we'd
376  * create or remove would then actually be
377  * /var/spool/mail/root.
378  * 
379  * To avoid this attack, we proceed as follows:
380  * 
381  * - First, follow symbolic links a la
382  *   dotlock_deference_symlink ().
383  * 
384  * - get the result's dirname.
385  * 
386  * - chdir to this directory.  If you can't, bail out.
387  * 
388  * - try to open the file in question, only using its
389  *   basename.  If you can't, bail out.
390  * 
391  * - fstat that file and compare the result to a
392  *   subsequent lstat (only using the basename).  If
393  *   the comparison fails, bail out.
394  * 
395  * dotlock_prepare () is invoked from main () directly
396  * after the command line parsing has been done.
397  *
398  * Return values:
399  * 
400  * 0 - Evereything's fine.  The program's new current
401  *     directory is the contains the file to be locked.
402  *     The string pointed to by bn contains the name of
403  *     the file to be locked.
404  * 
405  * -1 - Something failed. Don't continue.
406  * 
407  * tlr, Jul 15 1998
408  */
409
410 static int dotlock_check_stats (struct stat *fsb, struct stat *lsb)
411 {
412   /* S_ISLNK (fsb->st_mode) should actually be impossible,
413    * but we may have mixed up the parameters somewhere.
414    * play safe.
415    */
416
417   if (S_ISLNK (lsb->st_mode) || S_ISLNK (fsb->st_mode))
418     return -1;
419
420   if ((lsb->st_dev != fsb->st_dev) ||
421       (lsb->st_ino != fsb->st_ino) ||
422       (lsb->st_mode != fsb->st_mode) ||
423       (lsb->st_nlink != fsb->st_nlink) ||
424       (lsb->st_uid != fsb->st_uid) ||
425       (lsb->st_gid != fsb->st_gid) ||
426       (lsb->st_rdev != fsb->st_rdev) || (lsb->st_size != fsb->st_size)) {
427     /* something's fishy */
428     return -1;
429   }
430
431   return 0;
432 }
433
434 static int dotlock_prepare (char *bn, size_t l, const char *f, int _fd)
435 {
436   struct stat fsb, lsb;
437   char realpath[_POSIX_PATH_MAX];
438   char *basename, *dirname;
439   char *p;
440   int fd;
441   int r;
442
443   if (dotlock_deference_symlink (realpath, sizeof (realpath), f) == -1)
444     return -1;
445
446   if ((p = strrchr (realpath, '/'))) {
447     *p = '\0';
448     basename = p + 1;
449     dirname = realpath;
450   }
451   else {
452     basename = realpath;
453     dirname = ".";
454   }
455
456   if (mutt_strlen (basename) + 1 > l)
457     return -1;
458
459   strfcpy (bn, basename, l);
460
461   if (chdir (dirname) == -1)
462     return -1;
463
464   if (_fd != -1)
465     fd = _fd;
466   else if ((fd = open (basename, O_RDONLY)) == -1)
467     return -1;
468
469   r = fstat (fd, &fsb);
470
471   if (_fd == -1)
472     close (fd);
473
474   if (r == -1)
475     return -1;
476
477   if (lstat (basename, &lsb) == -1)
478     return -1;
479
480   if (dotlock_check_stats (&fsb, &lsb) == -1)
481     return -1;
482
483   return 0;
484 }
485
486 /*
487  * Expand a symbolic link.
488  * 
489  * This function expects newpath to have space for
490  * at least _POSIX_PATH_MAX characters.
491  *
492  */
493
494 static void
495 dotlock_expand_link (char *newpath, const char *path, const char *link)
496 {
497   const char *lb = NULL;
498   size_t len;
499
500   /* link is full path */
501   if (*link == '/') {
502     strfcpy (newpath, link, _POSIX_PATH_MAX);
503     return;
504   }
505
506   if ((lb = strrchr (path, '/')) == NULL) {
507     /* no path in link */
508     strfcpy (newpath, link, _POSIX_PATH_MAX);
509     return;
510   }
511
512   len = lb - path + 1;
513   memcpy (newpath, path, len);
514   strfcpy (newpath + len, link, _POSIX_PATH_MAX - len);
515 }
516
517
518 /*
519  * Deference a chain of symbolic links
520  * 
521  * The final path is written to d.
522  *
523  */
524
525 static int dotlock_deference_symlink (char *d, size_t l, const char *path)
526 {
527   struct stat sb;
528   char realpath[_POSIX_PATH_MAX];
529   const char *pathptr = path;
530   int count = 0;
531
532   while (count++ < MAXLINKS) {
533     if (lstat (pathptr, &sb) == -1) {
534       /* perror (pathptr); */
535       return -1;
536     }
537
538     if (S_ISLNK (sb.st_mode)) {
539       char linkfile[_POSIX_PATH_MAX];
540       char linkpath[_POSIX_PATH_MAX];
541       int len;
542
543       if ((len = readlink (pathptr, linkfile, sizeof (linkfile))) == -1) {
544         /* perror (pathptr); */
545         return -1;
546       }
547
548       linkfile[len] = '\0';
549       dotlock_expand_link (linkpath, pathptr, linkfile);
550       strfcpy (realpath, linkpath, sizeof (realpath));
551       pathptr = realpath;
552     }
553     else
554       break;
555   }
556
557   strfcpy (d, pathptr, l);
558   return 0;
559 }
560
561 /*
562  * Dotlock a file.
563  * 
564  * realpath is assumed _not_ to be an absolute path to
565  * the file we are about to lock.  Invoke
566  * dotlock_prepare () before using this function!
567  * 
568  */
569
570 #define HARDMAXATTEMPTS 10
571
572 static int dotlock_lock (const char *realpath)
573 {
574   char lockfile[_POSIX_PATH_MAX + LONG_STRING];
575   char nfslockfile[_POSIX_PATH_MAX + LONG_STRING];
576   size_t prev_size = 0;
577   int fd;
578   int count = 0;
579   int hard_count = 0;
580   struct stat sb;
581   time_t t;
582
583   snprintf (nfslockfile, sizeof (nfslockfile), "%s.%s.%d",
584             realpath, Hostname, (int) getpid ());
585   snprintf (lockfile, sizeof (lockfile), "%s.lock", realpath);
586
587
588   BEGIN_PRIVILEGED ();
589
590   unlink (nfslockfile);
591
592   while ((fd = open (nfslockfile, O_WRONLY | O_EXCL | O_CREAT, 0)) < 0) {
593     END_PRIVILEGED ();
594
595
596     if (errno != EAGAIN) {
597       /* perror ("cannot open NFS lock file"); */
598       return DL_EX_ERROR;
599     }
600
601
602     BEGIN_PRIVILEGED ();
603   }
604
605   END_PRIVILEGED ();
606
607
608   close (fd);
609
610   while (hard_count++ < HARDMAXATTEMPTS) {
611
612     BEGIN_PRIVILEGED ();
613     link (nfslockfile, lockfile);
614     END_PRIVILEGED ();
615
616     if (stat (nfslockfile, &sb) != 0) {
617       /* perror ("stat"); */
618       return DL_EX_ERROR;
619     }
620
621     if (sb.st_nlink == 2)
622       break;
623
624     if (count == 0)
625       prev_size = sb.st_size;
626
627     if (prev_size == sb.st_size && ++count > Retry) {
628       if (DotlockFlags & DL_FL_FORCE) {
629         BEGIN_PRIVILEGED ();
630         unlink (lockfile);
631         END_PRIVILEGED ();
632
633         count = 0;
634         continue;
635       }
636       else {
637         BEGIN_PRIVILEGED ();
638         unlink (nfslockfile);
639         END_PRIVILEGED ();
640         return DL_EX_EXIST;
641       }
642     }
643
644     prev_size = sb.st_size;
645
646     /* don't trust sleep (3) as it may be interrupted
647      * by users sending signals. 
648      */
649
650     t = time (NULL);
651     do {
652       sleep (1);
653     } while (time (NULL) == t);
654   }
655
656   BEGIN_PRIVILEGED ();
657   unlink (nfslockfile);
658   END_PRIVILEGED ();
659
660   return DL_EX_OK;
661 }
662
663
664 /*
665  * Unlock a file. 
666  * 
667  * The same comment as for dotlock_lock () applies here.
668  * 
669  */
670
671 static int dotlock_unlock (const char *realpath)
672 {
673   char lockfile[_POSIX_PATH_MAX + LONG_STRING];
674   int i;
675
676   snprintf (lockfile, sizeof (lockfile), "%s.lock", realpath);
677
678   BEGIN_PRIVILEGED ();
679   i = unlink (lockfile);
680   END_PRIVILEGED ();
681
682   if (i == -1)
683     return DL_EX_ERROR;
684
685   return DL_EX_OK;
686 }
687
688 /* remove an empty file */
689
690 static int dotlock_unlink (const char *realpath)
691 {
692   struct stat lsb;
693   int i = -1;
694
695   if (dotlock_lock (realpath) != DL_EX_OK)
696     return DL_EX_ERROR;
697
698   if ((i = lstat (realpath, &lsb)) == 0 && lsb.st_size == 0)
699     unlink (realpath);
700
701   dotlock_unlock (realpath);
702
703   return (i == 0) ? DL_EX_OK : DL_EX_ERROR;
704 }
705
706
707 /*
708  * Check if a file can be locked at all.
709  * 
710  * The same comment as for dotlock_lock () applies here.
711  * 
712  */
713
714 static int dotlock_try (void)
715 {
716 #ifdef USE_SETGID
717   struct stat sb;
718 #endif
719
720   if (access (".", W_OK) == 0)
721     return DL_EX_OK;
722
723 #ifdef USE_SETGID
724   if (stat (".", &sb) == 0) {
725     if ((sb.st_mode & S_IWGRP) == S_IWGRP && sb.st_gid == MailGid)
726       return DL_EX_NEED_PRIVS;
727   }
728 #endif
729
730   return DL_EX_IMPOSSIBLE;
731 }