mutt_*mktemp--
[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) if (a & DL_FL_ACTIONS) usage (argv[0])
72
73 int main (int argc, char **argv)
74 {
75   int i;
76   char *p;
77   struct utsname utsname;
78
79   /* first, drop privileges */
80
81   if (dotlock_init_privs () == -1)
82     return DL_EX_ERROR;
83
84
85   /* determine the system's host name */
86
87   uname (&utsname);
88   if (!(Hostname = strdup (utsname.nodename)))  /* __MEM_CHECKED__ */
89     return DL_EX_ERROR;
90   if ((p = strchr (Hostname, '.')))
91     *p = '\0';
92
93
94   /* parse the command line options. */
95   DotlockFlags = 0;
96
97   while ((i = getopt (argc, argv, "dtfupr:")) != EOF) {
98     switch (i) {
99       /* actions, mutually exclusive */
100     case 't':
101       check_flags (DotlockFlags);
102       DotlockFlags |= DL_FL_TRY;
103       break;
104     case 'd':
105       check_flags (DotlockFlags);
106       DotlockFlags |= DL_FL_UNLINK;
107       break;
108     case 'u':
109       check_flags (DotlockFlags);
110       DotlockFlags |= DL_FL_UNLOCK;
111       break;
112
113       /* other flags */
114     case 'f':
115       DotlockFlags |= DL_FL_FORCE;
116       break;
117     case 'p':
118       DotlockFlags |= DL_FL_USEPRIV;
119       break;
120     case 'r':
121       DotlockFlags |= DL_FL_RETRY;
122       Retry = atoi (optarg);
123       break;
124
125     default:
126       usage (argv[0]);
127     }
128   }
129
130   if (optind == argc || Retry < 0)
131     usage (argv[0]);
132
133   return dotlock_dispatch (argv[optind], -1);
134 }
135
136
137 /* 
138  * Determine our effective group ID, and drop 
139  * privileges.
140  * 
141  * Return value:
142  * 
143  *  0 - everything went fine
144  * -1 - we couldn't drop privileges.
145  * 
146  */
147
148
149 static int dotlock_init_privs (void)
150 {
151
152 # ifdef USE_SETGID
153
154   UserGid = getgid ();
155   MailGid = getegid ();
156
157   if (SETEGID (UserGid) != 0)
158     return -1;
159
160 # endif
161
162   return 0;
163 }
164
165
166 static int dotlock_dispatch (const char *f, int fd)
167 {
168   char frealpath[_POSIX_PATH_MAX];
169
170   /* If dotlock_prepare () succeeds [return value == 0],
171    * realpath contains the basename of f, and we have
172    * successfully changed our working directory to
173    * `dirname $f`.  Additionally, f has been opened for
174    * reading to verify that the user has at least read
175    * permissions on that file.
176    * 
177    * For a more detailed explanation of all this, see the
178    * lengthy comment below.
179    */
180
181   if (dotlock_prepare (frealpath, sizeof (frealpath), f, fd) != 0)
182     return DL_EX_ERROR;
183
184   /* Actually perform the locking operation. */
185
186   if (DotlockFlags & DL_FL_TRY)
187     return dotlock_try ();
188   else if (DotlockFlags & DL_FL_UNLOCK)
189     return dotlock_unlock (frealpath);
190   else if (DotlockFlags & DL_FL_UNLINK)
191     return dotlock_unlink (frealpath);
192   else                          /* lock */
193     return dotlock_lock (frealpath);
194 }
195
196
197 /*
198  * Get privileges 
199  * 
200  * This function re-acquires the privileges we may have
201  * if the user told us to do so by giving the "-p"
202  * command line option.
203  * 
204  * BEGIN_PRIVILEGES () won't return if an error occurs.
205  * 
206  */
207
208 static void BEGIN_PRIVILEGED (void)
209 {
210 #ifdef USE_SETGID
211   if (DotlockFlags & DL_FL_USEPRIV) {
212     if (SETEGID (MailGid) != 0) {
213       /* perror ("setegid"); */
214       exit (DL_EX_ERROR);
215     }
216   }
217 #endif
218 }
219
220 /*
221  * Drop privileges
222  * 
223  * This function drops the group privileges we may have.
224  * 
225  * END_PRIVILEGED () won't return if an error occurs.
226  *
227  */
228
229 static void END_PRIVILEGED (void)
230 {
231 #ifdef USE_SETGID
232   if (DotlockFlags & DL_FL_USEPRIV) {
233     if (SETEGID (UserGid) != 0) {
234       /* perror ("setegid"); */
235       exit (DL_EX_ERROR);
236     }
237   }
238 #endif
239 }
240
241 /*
242  * Usage information.
243  * 
244  * This function doesn't return.
245  * 
246  */
247
248 static void usage (const char *av0)
249 {
250   fprintf (stderr, "dotlock [Madmutt %s]\n", VERSION);
251   fprintf (stderr, "usage: %s [-t|-f|-u|-d] [-p] [-r <retries>] file\n", av0);
252
253   fputs ("\noptions:"
254          "\n  -t\t\ttry"
255          "\n  -f\t\tforce"
256          "\n  -u\t\tunlock" "\n  -d\t\tunlink" "\n  -p\t\tprivileged"
257 #ifndef USE_SETGID
258          " (ignored)"
259 #endif
260          "\n  -r <retries>\tRetry locking" "\n", stderr);
261
262   exit (DL_EX_ERROR);
263 }
264
265 /*
266  * Access checking: Let's avoid to lock other users' mail
267  * spool files if we aren't permitted to read them.
268  * 
269  * Some simple-minded access (2) checking isn't sufficient
270  * here: The problem is that the user may give us a
271  * deeply nested path to a file which has the same name
272  * as the file he wants to lock, but different
273  * permissions, say, e.g.
274  * /tmp/lots/of/subdirs/var/spool/mail/root.
275  * 
276  * He may then try to replace /tmp/lots/of/subdirs by a
277  * symbolic link to / after we have invoked access () to
278  * check the file's permissions.  The lockfile we'd
279  * create or remove would then actually be
280  * /var/spool/mail/root.
281  * 
282  * To avoid this attack, we proceed as follows:
283  * 
284  * - First, follow symbolic links a la
285  *   dotlock_deference_symlink ().
286  * 
287  * - get the result's dirname.
288  * 
289  * - chdir to this directory.  If you can't, bail out.
290  * 
291  * - try to open the file in question, only using its
292  *   basename.  If you can't, bail out.
293  * 
294  * - fstat that file and compare the result to a
295  *   subsequent lstat (only using the basename).  If
296  *   the comparison fails, bail out.
297  * 
298  * dotlock_prepare () is invoked from main () directly
299  * after the command line parsing has been done.
300  *
301  * Return values:
302  * 
303  * 0 - Evereything's fine.  The program's new current
304  *     directory is the contains the file to be locked.
305  *     The string pointed to by bn contains the name of
306  *     the file to be locked.
307  * 
308  * -1 - Something failed. Don't continue.
309  * 
310  * tlr, Jul 15 1998
311  */
312
313 static int dotlock_check_stats (struct stat *fsb, struct stat *lsb)
314 {
315   /* S_ISLNK (fsb->st_mode) should actually be impossible,
316    * but we may have mixed up the parameters somewhere.
317    * play safe.
318    */
319
320   if (S_ISLNK (lsb->st_mode) || S_ISLNK (fsb->st_mode))
321     return -1;
322
323   if ((lsb->st_dev != fsb->st_dev) ||
324       (lsb->st_ino != fsb->st_ino) ||
325       (lsb->st_mode != fsb->st_mode) ||
326       (lsb->st_nlink != fsb->st_nlink) ||
327       (lsb->st_uid != fsb->st_uid) ||
328       (lsb->st_gid != fsb->st_gid) ||
329       (lsb->st_rdev != fsb->st_rdev) || (lsb->st_size != fsb->st_size)) {
330     /* something's fishy */
331     return -1;
332   }
333
334   return 0;
335 }
336
337 static int dotlock_prepare (char *bn, ssize_t l, const char *f, int _fd)
338 {
339   struct stat fsb, lsb;
340   char frealpath[_POSIX_PATH_MAX];
341   char *fbasename, *dirname;
342   char *p;
343   int fd;
344   int r;
345
346   if (dotlock_deference_symlink (frealpath, sizeof (frealpath), f) == -1)
347     return -1;
348
349   if ((p = strrchr (frealpath, '/'))) {
350     *p = '\0';
351     fbasename = p + 1;
352     dirname = frealpath;
353   }
354   else {
355     fbasename = frealpath;
356     dirname = m_strdup(".");
357   }
358
359   if (m_strlen(fbasename) + 1 > l)
360     return -1;
361
362   m_strcpy(bn, l, fbasename);
363
364   if (chdir (dirname) == -1)
365     return -1;
366
367   if (_fd != -1)
368     fd = _fd;
369   else if ((fd = open (fbasename, O_RDONLY)) == -1)
370     return -1;
371
372   r = fstat (fd, &fsb);
373
374   if (_fd == -1)
375     close (fd);
376
377   if (r == -1)
378     return -1;
379
380   if (lstat (fbasename, &lsb) == -1)
381     return -1;
382
383   if (dotlock_check_stats (&fsb, &lsb) == -1)
384     return -1;
385
386   return 0;
387 }
388
389 /*
390  * Expand a symbolic link.
391  * 
392  * This function expects newpath to have space for
393  * at least _POSIX_PATH_MAX characters.
394  *
395  */
396
397 static void
398 dotlock_expand_link (char *newpath, const char *path, const char *flink)
399 {
400   const char *lb = NULL;
401   size_t len;
402
403   /* link is full path */
404   if (*flink == '/') {
405     m_strcpy(newpath, _POSIX_PATH_MAX, flink);
406     return;
407   }
408
409   if ((lb = strrchr (path, '/')) == NULL) {
410     /* no path in link */
411     m_strcpy(newpath, _POSIX_PATH_MAX, flink);
412     return;
413   }
414
415   len = lb - path + 1;
416   memcpy (newpath, path, len);
417   m_strcpy(newpath + len, _POSIX_PATH_MAX - len, flink);
418 }
419
420
421 /*
422  * Deference a chain of symbolic links
423  * 
424  * The final path is written to d.
425  *
426  */
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 }