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