Subject: mailbox locking for Solaris

From: Charles Hedrick <[email-masked]>
Date: Tue, 16 Jul 1996 17:53:32 -0400

Pine/imapd won't work at Rutgers without some modifications.  While
our mailbox configuration is somewhat non-standard, normal Sun mail
software will work without changes.  Thus I claim that you have a bug.
Fortunately for you, I have a fix. 

Here's the issue: Sun normally uses both methods of locking: they use
both kernel locking and lock files.  So does pine and imapd.  However
Sun's mail software all runs setgid to mail.  Pine does not.  Because
their software runs setgid, we are able to protect /var/mail so that
normal users can't create files in it.  This protects us from the
symlink attack.  (It is possible to defend yourself against that
attack to some extent by testing whether a symlink exists.  However
that code has race conditions in it.) Because pine doesn't run setgid,
it can't create lock files on our systems.  That causes a big problem
for us, because we disable kernel locking.  We do this because of the
long history of problems with lockd. 

Anyway, I claim that pine needs two changes:
 - it needs to be able to run setgid mail to create lock files
 - it should be possible to disable kernel locking in a configuration
	file for pine and an option for imap

I want to be able to control kernel locking at runtime because I want
to be able to use a single binary throughout Rutgers.  Some Rutgers
hosts use normal Sun conventions.  For them kernel locking should be
on.  The larger systems want to disable it.  Since locking is
controlled at runtime, this should be easy.  The mechanism is already
there: you just need to add it to the list of things that can be in
the config file (presumably config.fixed), and add an option for imap. 

Creating lock files is more complex.  Pine does too many "system" and
"fork/exec" calls for me to feel safe in simply running it setgid
mail.  Instead, the approach I've taken is to create a small helper
program to lock and unlock mail files: /usr/local/lib/maillock.  This
helper program runs setgid.  Thus the modifications to c-client are to
check which file is being locked, and call the helper program when
it's the user's mail file. 

Enclosed you'll find two things: diffs to c-client to call maillock,
and maillock itself.  This code has been tested under Solaris 2.4
(gso) and Digital Unix 3.2c and 4.0 (osf).  Maillock is based on our
copy of Berkeley Mail.  I thought it was free, but I have a suspicion
that this particular file may actually have gone through ATT.  It
might be safer to go to a version from freebsd or some other public
source.  If you take these changes I'll get you a version of maillock
that I know is unencumbered. 

*** 1.1	1996/07/16 00:19:19
--- imap/non-ANSI/c-client/bezerk.c	1996/07/16 21:28:32
***************
*** 58,63 ****
--- 58,71 ----
  #include "rfc822.h"
  #include "misc.h"
  #include "dummy.h"
+ 
+ #define USE_LOCKPROGRAM 1
+ #ifdef USE_LOCKPROGRAM
+ #include <pwd.h>
+ #include <unistd.h>
+ #include <sys/wait.h>
+ #endif
+ 
  
  /* Berkeley mail routines */
  
***************
*** 1208,1213 ****
--- 1216,1312 ----
    }
  }
  
+ #ifdef USE_LOCKPROGRAM
+ int ru_samefile (char *file1, char *file2)
+ {
+   struct stat stat1, stat2;
+ 
+   if (stat(file1, &stat1) == 0 && stat(file2, &stat2) == 0) {
+     if (stat1.st_dev == stat2.st_dev && stat1.st_ino == stat2.st_ino)
+       return 1;
+     return 0;
+   } 
+   if (strcmp(file1, file2) == 0)
+     return 1;
+   return 0;
+ }
+ 
+ int ru_dolock (char *file,int flags,int mode,char *lock,int op)
+ {
+   int fd,j;
+   char tmp[MAILTMPLEN];
+   int pid, statusloc;
+ 
+   pid = fork();
+   if (pid < 0) {
+     sprintf (tmp,"Can't run mail lock program: %s", strerror(errno));
+     mm_log (tmp,WARN);	/* this is probably not good */
+     return -1;
+   }
+   
+   if (pid == 0) {
+     execl("/usr/local/lib/maillock", "maillock", myusername(), NULL);
+     exit(2);
+   }	 
+   
+   while(waitpid(pid, &statusloc, 0) == -1 && errno == EINTR)
+     ;
+   if (WIFEXITED(statusloc)) {
+     if (WEXITSTATUS(statusloc) == 0) {
+       if ((fd = open (file,flags,mode)) >= 0) flock (fd,op);
+       else {			/* open failed */
+ 	j = errno;			/* preserve error code */
+ 	ru_unlock(0);
+ 	errno = j;			/* restore error code */
+       }
+       return fd;
+     } else if (WEXITSTATUS(statusloc) == 1)
+ 	sprintf (tmp,"Can't lock your mail file.  Try again in 5 minutes.");
+     else
+       sprintf (tmp,"Can't lock your mail file.  Contact your system administrator.");
+   } else
+     sprintf (tmp,"Can't lock your mail file.  Contact your system administrator.");
+ 
+   mm_log (tmp,WARN);	/* this is probably not good */
+   return -1;
+ }
+ 
+ int ru_unlock (warn)
+   int warn;
+ {
+   char tmp[MAILTMPLEN];
+   int pid, statusloc;
+ 
+   pid = fork();
+   if (pid < 0) {
+     if (warn) {
+       sprintf (tmp,"Can't run mail unlock program: %s", strerror(errno));
+       mm_log (tmp,WARN);	/* this is probably not good */
+     }
+     return -1;
+   }
+   
+   if (pid == 0) {
+     execl("/usr/local/lib/maillock", "maillock", "-u", myusername(), NULL);
+     exit(2);
+   }	 
+   
+   while(waitpid(pid, &statusloc, 0) == -1 && errno == EINTR)
+     ;
+   if (!warn)
+     return 0;
+ 
+   if (WIFEXITED(statusloc))
+     if (WEXITSTATUS(statusloc) == 0)
+       return 0;
+ 
+   sprintf (tmp,"Can't unlock your mail file.");
+   mm_log (tmp,WARN);	/* this is probably not good */
+   return -1;
+ }
+ #endif
+ 
+ 
  /* Berkeley open and lock mailbox
   * Accepts: file name to open/lock
   *	    file open mode
***************
*** 1227,1234 ****
    char hitch[MAILTMPLEN],tmp[MAILTMPLEN];
    time_t t;
    struct stat sb;
  				/* build lock filename */
!   if (chk_notsymlink (strcat (dummy_file (lock,file),".lock"))) do {
      t = time (0);		/* get the time now */
  #ifdef NFSKLUDGE
    /* SUN-OS had an NFS, As kludgy as an albatross;
--- 1326,1346 ----
    char hitch[MAILTMPLEN],tmp[MAILTMPLEN];
    time_t t;
    struct stat sb;
+ #ifdef USE_LOCKPROGRAM
+   char *requested;
+ 
+   requested = dummy_file(hitch,file);
+   sprintf (tmp,"%s/%s",MAILSPOOL,myusername());
+ #endif
+ 
  				/* build lock filename */
!   if (chk_notsymlink (strcat (dummy_file (lock,file),".lock"))) 
! #ifdef USE_LOCKPROGRAM
!    if (ru_samefile(requested, tmp))
!      return ru_dolock(file, flags, mode, lock, op);
!    else
! #endif
!     do {
      t = time (0);		/* get the time now */
  #ifdef NFSKLUDGE
    /* SUN-OS had an NFS, As kludgy as an albatross;
***************
*** 1334,1339 ****
--- 1446,1457 ----
  {
    struct stat sbuf;
    time_t tp[2];
+ #ifdef USE_LOCKPROGRAM
+   char tmp[MAILTMPLEN];
+ 
+   sprintf (tmp,"%s/%s.lock",MAILSPOOL,myusername());
+ #endif
+ 
    fstat (fd,&sbuf);		/* get file times */
  				/* if stream and csh would think new mail */
    if (stream && (sbuf.st_atime <= sbuf.st_mtime)) {
***************
*** 1346,1352 ****
    flock (fd,LOCK_UN);		/* release flock'ers */
    close (fd);			/* close the file */
  				/* flush the lock file if any */
!   if (lock && *lock) unlink (lock);
  }
  
  /* Berkeley mail parse and lock mailbox
--- 1464,1476 ----
    flock (fd,LOCK_UN);		/* release flock'ers */
    close (fd);			/* close the file */
  				/* flush the lock file if any */
!   if (lock && *lock) 
! #ifdef USE_LOCKPROGRAM
!     if (ru_samefile(lock, tmp))
!       ru_unlock(1);
!     else
! #endif
!       unlink (lock);
  }
  
  /* Berkeley mail parse and lock mailbox
*** 1.1	1996/07/15 19:32:13
--- imap/ANSI/c-client/bezerk.c	1996/07/16 21:28:57
***************
*** 58,63 ****
--- 58,71 ----
  #include "rfc822.h"
  #include "misc.h"
  #include "dummy.h"
+ 
+ #define USE_LOCKPROGRAM 1
+ #ifdef USE_LOCKPROGRAM
+ #include <pwd.h>
+ #include <unistd.h>
+ #include <sys/wait.h>
+ #endif
+ 
  
  /* Berkeley mail routines */
  
***************
*** 1127,1132 ****
--- 1135,1231 ----
    }
  }
  
+ #ifdef USE_LOCKPROGRAM
+ int ru_samefile (char *file1, char *file2)
+ {
+   struct stat stat1, stat2;
+ 
+   if (stat(file1, &stat1) == 0 && stat(file2, &stat2) == 0) {
+     if (stat1.st_dev == stat2.st_dev && stat1.st_ino == stat2.st_ino)
+       return 1;
+     return 0;
+   } 
+   if (strcmp(file1, file2) == 0)
+     return 1;
+   return 0;
+ }
+ 
+ int ru_dolock (char *file,int flags,int mode,char *lock,int op)
+ {
+   int fd,j;
+   char tmp[MAILTMPLEN];
+   int pid, statusloc;
+ 
+   pid = fork();
+   if (pid < 0) {
+     sprintf (tmp,"Can't run mail lock program: %s", strerror(errno));
+     mm_log (tmp,WARN);	/* this is probably not good */
+     return -1;
+   }
+   
+   if (pid == 0) {
+     execl("/usr/local/lib/maillock", "maillock", myusername(), NULL);
+     exit(2);
+   }	 
+   
+   while(waitpid(pid, &statusloc, 0) == -1 && errno == EINTR)
+     ;
+   if (WIFEXITED(statusloc)) {
+     if (WEXITSTATUS(statusloc) == 0) {
+       if ((fd = open (file,flags,mode)) >= 0) flock (fd,op);
+       else {			/* open failed */
+ 	j = errno;			/* preserve error code */
+ 	ru_unlock(0);
+ 	errno = j;			/* restore error code */
+       }
+       return fd;
+     } else if (WEXITSTATUS(statusloc) == 1)
+ 	sprintf (tmp,"Can't lock your mail file.  Try again in 5 minutes.");
+     else
+       sprintf (tmp,"Can't lock your mail file.  Contact your system administrator.");
+   } else
+     sprintf (tmp,"Can't lock your mail file.  Contact your system administrator.");
+ 
+   mm_log (tmp,WARN);	/* this is probably not good */
+   return -1;
+ }
+ 
+ int ru_unlock (warn)
+   int warn;
+ {
+   char tmp[MAILTMPLEN];
+   int pid, statusloc;
+ 
+   pid = fork();
+   if (pid < 0) {
+     if (warn) {
+       sprintf (tmp,"Can't run mail unlock program: %s", strerror(errno));
+       mm_log (tmp,WARN);	/* this is probably not good */
+     }
+     return -1;
+   }
+   
+   if (pid == 0) {
+     execl("/usr/local/lib/maillock", "maillock", "-u", myusername(), NULL);
+     exit(2);
+   }	 
+   
+   while(waitpid(pid, &statusloc, 0) == -1 && errno == EINTR)
+     ;
+   if (!warn)
+     return 0;
+ 
+   if (WIFEXITED(statusloc))
+     if (WEXITSTATUS(statusloc) == 0)
+       return 0;
+ 
+   sprintf (tmp,"Can't unlock your mail file.");
+   mm_log (tmp,WARN);	/* this is probably not good */
+   return -1;
+ }
+ #endif
+ 
+ 
  /* Berkeley open and lock mailbox
   * Accepts: file name to open/lock
   *	    file open mode
***************
*** 1141,1148 ****
    char hitch[MAILTMPLEN],tmp[MAILTMPLEN];
    time_t t;
    struct stat sb;
  				/* build lock filename */
!   if (chk_notsymlink (strcat (dummy_file (lock,file),".lock"))) do {
      t = time (0);		/* get the time now */
  #ifdef NFSKLUDGE
    /* SUN-OS had an NFS, As kludgy as an albatross;
--- 1240,1260 ----
    char hitch[MAILTMPLEN],tmp[MAILTMPLEN];
    time_t t;
    struct stat sb;
+ #ifdef USE_LOCKPROGRAM
+   char *requested;
+ 
+   requested = dummy_file(hitch,file);
+   sprintf (tmp,"%s/%s",MAILSPOOL,myusername());
+ #endif
+ 
  				/* build lock filename */
!   if (chk_notsymlink (strcat (dummy_file (lock,file),".lock"))) 
! #ifdef USE_LOCKPROGRAM
!    if (ru_samefile(requested, tmp))
!      return ru_dolock(file, flags, mode, lock, op);
!    else
! #endif
!     do {
      t = time (0);		/* get the time now */
  #ifdef NFSKLUDGE
    /* SUN-OS had an NFS, As kludgy as an albatross;
***************
*** 1245,1250 ****
--- 1357,1368 ----
  {
    struct stat sbuf;
    time_t tp[2];
+ #ifdef USE_LOCKPROGRAM
+   char tmp[MAILTMPLEN];
+ 
+   sprintf (tmp,"%s/%s.lock",MAILSPOOL,myusername());
+ #endif
+ 
    fstat (fd,&sbuf);		/* get file times */
  				/* if stream and csh would think new mail */
    if (stream && (sbuf.st_atime <= sbuf.st_mtime)) {
***************
*** 1257,1263 ****
    flock (fd,LOCK_UN);		/* release flock'ers */
    close (fd);			/* close the file */
  				/* flush the lock file if any */
!   if (lock && *lock) unlink (lock);
  }
  
  /* Berkeley mail parse and lock mailbox
--- 1375,1387 ----
    flock (fd,LOCK_UN);		/* release flock'ers */
    close (fd);			/* close the file */
  				/* flush the lock file if any */
!   if (lock && *lock) 
! #ifdef USE_LOCKPROGRAM
!     if (ru_samefile(lock, tmp))
!       ru_unlock(1);
!     else
! #endif
!       unlink (lock);
  }
  
  /* Berkeley mail parse and lock mailbox

-------------

maillock.c:

/*
 *
 * Stuff to do version 7 style locking.
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <pwd.h>

#define PATHSIZE 256
char	*maillock	= ".lock";		/* Lock suffix for mailname */
static	int		lockfd;			/* fd of mail file for flock */
static	char		curlock[PATHSIZE];	/* Last used name of lock */
static	int		locked;			/* To note that we locked it */
static	time_t		locktime;		/* time lock file was touched */
static  int		gidisset;		/* we've done setegid */

usage()
{
  fprintf(stderr, "Usage: maillock [-u] username\n");
  exit(1);
}

main(argc, argv)
  int argc;
  char **argv;
{
  int dounlock = 0;
  struct passwd *pwd;
  uid_t myuid;
  char namebuf[256];

  if (argc >= 2 && *argv[1] == '-') {
    if (strcmp (argv[1], "-u") != 0)
      exit(2);
    dounlock = 1;
    argc--;
    argv++;
  }

  if (argc < 2 || !*argv[1])
    usage();

  pwd = getpwnam(argv[1]);
  if (!pwd)
    exit(2);
  myuid = getuid();
  if (pwd->pw_uid != myuid && myuid != 0)
    exit(2);

  sprintf(namebuf, "%s/%s", MAILSPOOL, argv[1]);
  
  if (myuid == 0)
    setuid(pwd->pw_uid); /* let the user own the lock file */

  if (dounlock)
    exit(unlock(namebuf));
  else
    exit(lock(namebuf));
}

/*
 * Lock the specified mail file by setting the file mailfile.lock.
 * We must, of course, be careful to remove the lock file by a call
 * to unlock before we stop.  The algorithm used here is to see if
 * the lock exists, and if it does, to check its modify time.  If it
 * is older than 5 minutes, we assume error and set our own file.
 * Otherwise, we wait for 5 seconds and try again.
 */

lock(file)
char *file;
{
	register time_t t;
	struct stat sbuf;
	int statfailed;
	char locktmp[PATHSIZE];	/* Usable lock temporary */

	if (file == NULL) {
		return(2);
	}
	strcpy(curlock, file);
	strcat(curlock, maillock);
	strcpy(locktmp, file);
	strcat(locktmp, "XXXXXX");
	mktemp(locktmp);
	remove(locktmp);
	statfailed = 0;
	for (;;) {
		t = lock1(locktmp, curlock);
		if (t == 0) {
			locked = 1;
			locktime = time(0);
#ifdef DOFLOCK
			if ((lockfd = open(file, 0)) >= 0)
				flock(lockfd, LOCK_EX);
#endif
			return(0);
		}
		if (stat(curlock, &sbuf) < 0) {
			if (statfailed++ > 6)
				return(1);
			sleep(5);
			continue;
		}

		/*
		 * Compare the time of the temp file with the time
		 * of the lock file, rather than with the current
		 * time of day, since the files may reside on
		 * another machine whose time of day differs from
		 * ours.  If the lock file is less than 5 minutes
		 * old, keep trying.
		 */
		if (t < sbuf.st_ctime + 300) {
			if (statfailed++ > 6)
				return(1);
			sleep(5);
			continue;
		}
		remove(curlock);
/*
 * Open and then close the file in order to force NFS to get current
 * attributes.  Otherwise the code in quit may lose messages
 */
		t = open(file,0);
		if (t >= 0)
		  close(t);
	}
}

/*
 * Remove the mail lock, and note that we no longer
 * have it locked.
 */

unlock(file)
  char *file;
{

	strcpy(curlock, file);
	strcat(curlock, maillock);
#ifdef USEFLOCK
	flock(lockfd, LOCK_UN);
	close(lockfd);
	lockfd = -1;
#endif
	remove(curlock);
	locked = 0;
	return(0);
}

/*
 * Attempt to set the lock by creating the temporary file,
 * then doing a link/unlink.  If it succeeds, return 0,
 * else return a guess of the current time on the machine
 * holding the file.
 */

lock1(tempfile, name)
	char tempfile[], name[];
{
	register int fd;
	struct stat sbuf;

	fd = creat(tempfile, 0);
	if (fd < 0)
		return(time(0));
	fstat(fd, &sbuf);
	close(fd);
	if (link(tempfile, name) < 0) {
		remove(tempfile);
		return(sbuf.st_ctime);
	}
	remove(tempfile);
	return(0);
}

/*
 * Update the change time on the lock file so
 * others will know we're still using it.
 */

touchlock()
{
	struct stat sbuf;
	time_t t, tp[2];

	if (!locked)
		return;

	/* if it hasn't been at least 3 minutes, don't bother */
	if (time(&t) < locktime + 180)
		return;
	locktime = t;

	if (stat(curlock, &sbuf) < 0)
		return;
	/*
	 * Don't actually change the times, we just want the
	 * side effect that utime causes st_ctime to be set
	 * to the current time.
	 */
	tp[0] = sbuf.st_atime;
	tp[1] = sbuf.st_mtime;
	utime(curlock, tp);
}