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