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