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