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