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