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