useless blanking
[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 static int dotlock_deference_symlink (char *d, size_t l, const char *path)
429 {
430   struct stat sb;
431   char frealpath[_POSIX_PATH_MAX];
432   const char *pathptr = path;
433   int count = 0;
434
435   while (count++ < MAXLINKS) {
436     if (lstat (pathptr, &sb) == -1) {
437       /* perror (pathptr); */
438       return -1;
439     }
440
441     if (S_ISLNK (sb.st_mode)) {
442       char linkfile[_POSIX_PATH_MAX];
443       char linkpath[_POSIX_PATH_MAX];
444       int len;
445
446       if ((len = readlink (pathptr, linkfile, sizeof (linkfile))) == -1) {
447         /* perror (pathptr); */
448         return -1;
449       }
450
451       linkfile[len] = '\0';
452       dotlock_expand_link (linkpath, pathptr, linkfile);
453       m_strcpy(frealpath, sizeof(frealpath), linkpath);
454       pathptr = frealpath;
455     }
456     else
457       break;
458   }
459
460   m_strcpy(d, l, pathptr);
461   return 0;
462 }
463
464 /*
465  * Dotlock a file.
466  * 
467  * realpath is assumed _not_ to be an absolute path to
468  * the file we are about to lock.  Invoke
469  * dotlock_prepare () before using this function!
470  * 
471  */
472
473 #define HARDMAXATTEMPTS 10
474
475 static int dotlock_lock (const char *frealpath)
476 {
477   char lockfile[_POSIX_PATH_MAX + LONG_STRING];
478   char nfslockfile[_POSIX_PATH_MAX + LONG_STRING];
479   ssize_t prev_size = 0;
480   int fd;
481   int count = 0;
482   int hard_count = 0;
483   struct stat sb;
484   time_t t;
485
486   snprintf (nfslockfile, sizeof (nfslockfile), "%s.%s.%d",
487             frealpath, Hostname, (int) getpid ());
488   snprintf (lockfile, sizeof (lockfile), "%s.lock", frealpath);
489
490
491   BEGIN_PRIVILEGED ();
492
493   unlink (nfslockfile);
494
495   while ((fd = open (nfslockfile, O_WRONLY | O_EXCL | O_CREAT, 0)) < 0) {
496     END_PRIVILEGED ();
497
498
499     if (errno != EAGAIN) {
500       /* perror ("cannot open NFS lock file"); */
501       return DL_EX_ERROR;
502     }
503
504
505     BEGIN_PRIVILEGED ();
506   }
507
508   END_PRIVILEGED ();
509
510
511   close (fd);
512
513   while (hard_count++ < HARDMAXATTEMPTS) {
514
515     BEGIN_PRIVILEGED ();
516     link (nfslockfile, lockfile);
517     END_PRIVILEGED ();
518
519     if (stat (nfslockfile, &sb) != 0) {
520       /* perror ("stat"); */
521       return DL_EX_ERROR;
522     }
523
524     if (sb.st_nlink == 2)
525       break;
526
527     if (count == 0)
528       prev_size = sb.st_size;
529
530     if (prev_size == sb.st_size && ++count > Retry) {
531       if (DotlockFlags & DL_FL_FORCE) {
532         BEGIN_PRIVILEGED ();
533         unlink (lockfile);
534         END_PRIVILEGED ();
535
536         count = 0;
537         continue;
538       }
539       else {
540         BEGIN_PRIVILEGED ();
541         unlink (nfslockfile);
542         END_PRIVILEGED ();
543         return DL_EX_EXIST;
544       }
545     }
546
547     prev_size = sb.st_size;
548
549     /* don't trust sleep (3) as it may be interrupted
550      * by users sending signals. 
551      */
552
553     t = time (NULL);
554     do {
555       sleep (1);
556     } while (time (NULL) == t);
557   }
558
559   BEGIN_PRIVILEGED ();
560   unlink (nfslockfile);
561   END_PRIVILEGED ();
562
563   return DL_EX_OK;
564 }
565
566
567 /*
568  * Unlock a file. 
569  * 
570  * The same comment as for dotlock_lock () applies here.
571  * 
572  */
573
574 static int dotlock_unlock (const char *frealpath)
575 {
576   char lockfile[_POSIX_PATH_MAX + LONG_STRING];
577   int i;
578
579   snprintf (lockfile, sizeof (lockfile), "%s.lock", frealpath);
580
581   BEGIN_PRIVILEGED ();
582   i = unlink (lockfile);
583   END_PRIVILEGED ();
584
585   if (i == -1)
586     return DL_EX_ERROR;
587
588   return DL_EX_OK;
589 }
590
591 /* remove an empty file */
592
593 static int dotlock_unlink (const char *frealpath)
594 {
595   struct stat lsb;
596   int i = -1;
597
598   if (dotlock_lock (frealpath) != DL_EX_OK)
599     return DL_EX_ERROR;
600
601   if ((i = lstat (frealpath, &lsb)) == 0 && lsb.st_size == 0)
602     unlink (frealpath);
603
604   dotlock_unlock (frealpath);
605
606   return (i == 0) ? DL_EX_OK : DL_EX_ERROR;
607 }
608
609
610 /*
611  * Check if a file can be locked at all.
612  * 
613  * The same comment as for dotlock_lock () applies here.
614  * 
615  */
616
617 static int dotlock_try (void)
618 {
619 #ifdef USE_SETGID
620   struct stat sb;
621 #endif
622
623   if (access (".", W_OK) == 0)
624     return DL_EX_OK;
625
626 #ifdef USE_SETGID
627   if (stat (".", &sb) == 0) {
628     if ((sb.st_mode & S_IWGRP) == S_IWGRP && sb.st_gid == MailGid)
629       return DL_EX_NEED_PRIVS;
630   }
631 #endif
632
633   return DL_EX_IMPOSSIBLE;
634 }