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