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