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