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