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